diff options
author | Chocobozzz <florian.bigard@gmail.com> | 2017-05-15 22:22:03 +0200 |
---|---|---|
committer | Chocobozzz <florian.bigard@gmail.com> | 2017-05-20 09:57:40 +0200 |
commit | 65fcc3119c334b75dd13bcfdebf186afdc580a8f (patch) | |
tree | 4f2158c61a9b7c3f47cfa233d01413b946ee53c0 /server/models/video.js | |
parent | d5f345ed4cfac4e1fa84dcb4fce1cda4d32f9c73 (diff) | |
download | PeerTube-65fcc3119c334b75dd13bcfdebf186afdc580a8f.tar.gz PeerTube-65fcc3119c334b75dd13bcfdebf186afdc580a8f.tar.zst PeerTube-65fcc3119c334b75dd13bcfdebf186afdc580a8f.zip |
First typescript iteration
Diffstat (limited to 'server/models/video.js')
-rw-r--r-- | server/models/video.js | 858 |
1 files changed, 0 insertions, 858 deletions
diff --git a/server/models/video.js b/server/models/video.js deleted file mode 100644 index da4ddb420..000000000 --- a/server/models/video.js +++ /dev/null | |||
@@ -1,858 +0,0 @@ | |||
1 | 'use strict' | ||
2 | |||
3 | const Buffer = require('safe-buffer').Buffer | ||
4 | const createTorrent = require('create-torrent') | ||
5 | const ffmpeg = require('fluent-ffmpeg') | ||
6 | const fs = require('fs') | ||
7 | const magnetUtil = require('magnet-uri') | ||
8 | const map = require('lodash/map') | ||
9 | const parallel = require('async/parallel') | ||
10 | const series = require('async/series') | ||
11 | const parseTorrent = require('parse-torrent') | ||
12 | const pathUtils = require('path') | ||
13 | const values = require('lodash/values') | ||
14 | |||
15 | const constants = require('../initializers/constants') | ||
16 | const logger = require('../helpers/logger') | ||
17 | const friends = require('../lib/friends') | ||
18 | const modelUtils = require('./utils') | ||
19 | const customVideosValidators = require('../helpers/custom-validators').videos | ||
20 | const db = require('../initializers/database') | ||
21 | const jobScheduler = require('../lib/jobs/job-scheduler') | ||
22 | |||
23 | // --------------------------------------------------------------------------- | ||
24 | |||
25 | module.exports = function (sequelize, DataTypes) { | ||
26 | const Video = sequelize.define('Video', | ||
27 | { | ||
28 | id: { | ||
29 | type: DataTypes.UUID, | ||
30 | defaultValue: DataTypes.UUIDV4, | ||
31 | primaryKey: true, | ||
32 | validate: { | ||
33 | isUUID: 4 | ||
34 | } | ||
35 | }, | ||
36 | name: { | ||
37 | type: DataTypes.STRING, | ||
38 | allowNull: false, | ||
39 | validate: { | ||
40 | nameValid: function (value) { | ||
41 | const res = customVideosValidators.isVideoNameValid(value) | ||
42 | if (res === false) throw new Error('Video name is not valid.') | ||
43 | } | ||
44 | } | ||
45 | }, | ||
46 | extname: { | ||
47 | type: DataTypes.ENUM(values(constants.CONSTRAINTS_FIELDS.VIDEOS.EXTNAME)), | ||
48 | allowNull: false | ||
49 | }, | ||
50 | remoteId: { | ||
51 | type: DataTypes.UUID, | ||
52 | allowNull: true, | ||
53 | validate: { | ||
54 | isUUID: 4 | ||
55 | } | ||
56 | }, | ||
57 | category: { | ||
58 | type: DataTypes.INTEGER, | ||
59 | allowNull: false, | ||
60 | validate: { | ||
61 | categoryValid: function (value) { | ||
62 | const res = customVideosValidators.isVideoCategoryValid(value) | ||
63 | if (res === false) throw new Error('Video category is not valid.') | ||
64 | } | ||
65 | } | ||
66 | }, | ||
67 | licence: { | ||
68 | type: DataTypes.INTEGER, | ||
69 | allowNull: false, | ||
70 | defaultValue: null, | ||
71 | validate: { | ||
72 | licenceValid: function (value) { | ||
73 | const res = customVideosValidators.isVideoLicenceValid(value) | ||
74 | if (res === false) throw new Error('Video licence is not valid.') | ||
75 | } | ||
76 | } | ||
77 | }, | ||
78 | language: { | ||
79 | type: DataTypes.INTEGER, | ||
80 | allowNull: true, | ||
81 | validate: { | ||
82 | languageValid: function (value) { | ||
83 | const res = customVideosValidators.isVideoLanguageValid(value) | ||
84 | if (res === false) throw new Error('Video language is not valid.') | ||
85 | } | ||
86 | } | ||
87 | }, | ||
88 | nsfw: { | ||
89 | type: DataTypes.BOOLEAN, | ||
90 | allowNull: false, | ||
91 | validate: { | ||
92 | nsfwValid: function (value) { | ||
93 | const res = customVideosValidators.isVideoNSFWValid(value) | ||
94 | if (res === false) throw new Error('Video nsfw attribute is not valid.') | ||
95 | } | ||
96 | } | ||
97 | }, | ||
98 | description: { | ||
99 | type: DataTypes.STRING, | ||
100 | allowNull: false, | ||
101 | validate: { | ||
102 | descriptionValid: function (value) { | ||
103 | const res = customVideosValidators.isVideoDescriptionValid(value) | ||
104 | if (res === false) throw new Error('Video description is not valid.') | ||
105 | } | ||
106 | } | ||
107 | }, | ||
108 | infoHash: { | ||
109 | type: DataTypes.STRING, | ||
110 | allowNull: false, | ||
111 | validate: { | ||
112 | infoHashValid: function (value) { | ||
113 | const res = customVideosValidators.isVideoInfoHashValid(value) | ||
114 | if (res === false) throw new Error('Video info hash is not valid.') | ||
115 | } | ||
116 | } | ||
117 | }, | ||
118 | duration: { | ||
119 | type: DataTypes.INTEGER, | ||
120 | allowNull: false, | ||
121 | validate: { | ||
122 | durationValid: function (value) { | ||
123 | const res = customVideosValidators.isVideoDurationValid(value) | ||
124 | if (res === false) throw new Error('Video duration is not valid.') | ||
125 | } | ||
126 | } | ||
127 | }, | ||
128 | views: { | ||
129 | type: DataTypes.INTEGER, | ||
130 | allowNull: false, | ||
131 | defaultValue: 0, | ||
132 | validate: { | ||
133 | min: 0, | ||
134 | isInt: true | ||
135 | } | ||
136 | }, | ||
137 | likes: { | ||
138 | type: DataTypes.INTEGER, | ||
139 | allowNull: false, | ||
140 | defaultValue: 0, | ||
141 | validate: { | ||
142 | min: 0, | ||
143 | isInt: true | ||
144 | } | ||
145 | }, | ||
146 | dislikes: { | ||
147 | type: DataTypes.INTEGER, | ||
148 | allowNull: false, | ||
149 | defaultValue: 0, | ||
150 | validate: { | ||
151 | min: 0, | ||
152 | isInt: true | ||
153 | } | ||
154 | } | ||
155 | }, | ||
156 | { | ||
157 | indexes: [ | ||
158 | { | ||
159 | fields: [ 'authorId' ] | ||
160 | }, | ||
161 | { | ||
162 | fields: [ 'remoteId' ] | ||
163 | }, | ||
164 | { | ||
165 | fields: [ 'name' ] | ||
166 | }, | ||
167 | { | ||
168 | fields: [ 'createdAt' ] | ||
169 | }, | ||
170 | { | ||
171 | fields: [ 'duration' ] | ||
172 | }, | ||
173 | { | ||
174 | fields: [ 'infoHash' ] | ||
175 | }, | ||
176 | { | ||
177 | fields: [ 'views' ] | ||
178 | }, | ||
179 | { | ||
180 | fields: [ 'likes' ] | ||
181 | } | ||
182 | ], | ||
183 | classMethods: { | ||
184 | associate, | ||
185 | |||
186 | generateThumbnailFromData, | ||
187 | getDurationFromFile, | ||
188 | list, | ||
189 | listForApi, | ||
190 | listOwnedAndPopulateAuthorAndTags, | ||
191 | listOwnedByAuthor, | ||
192 | load, | ||
193 | loadByHostAndRemoteId, | ||
194 | loadAndPopulateAuthor, | ||
195 | loadAndPopulateAuthorAndPodAndTags, | ||
196 | searchAndPopulateAuthorAndPodAndTags | ||
197 | }, | ||
198 | instanceMethods: { | ||
199 | generateMagnetUri, | ||
200 | getVideoFilename, | ||
201 | getThumbnailName, | ||
202 | getPreviewName, | ||
203 | getTorrentName, | ||
204 | isOwned, | ||
205 | toFormatedJSON, | ||
206 | toAddRemoteJSON, | ||
207 | toUpdateRemoteJSON, | ||
208 | transcodeVideofile, | ||
209 | removeFromBlacklist | ||
210 | }, | ||
211 | hooks: { | ||
212 | beforeValidate, | ||
213 | beforeCreate, | ||
214 | afterDestroy | ||
215 | } | ||
216 | } | ||
217 | ) | ||
218 | |||
219 | return Video | ||
220 | } | ||
221 | |||
222 | function beforeValidate (video, options, next) { | ||
223 | // Put a fake infoHash if it does not exists yet | ||
224 | if (video.isOwned() && !video.infoHash) { | ||
225 | // 40 hexa length | ||
226 | video.infoHash = '0123456789abcdef0123456789abcdef01234567' | ||
227 | } | ||
228 | |||
229 | return next(null) | ||
230 | } | ||
231 | |||
232 | function beforeCreate (video, options, next) { | ||
233 | const tasks = [] | ||
234 | |||
235 | if (video.isOwned()) { | ||
236 | const videoPath = pathUtils.join(constants.CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename()) | ||
237 | |||
238 | tasks.push( | ||
239 | function createVideoTorrent (callback) { | ||
240 | createTorrentFromVideo(video, videoPath, callback) | ||
241 | }, | ||
242 | |||
243 | function createVideoThumbnail (callback) { | ||
244 | createThumbnail(video, videoPath, callback) | ||
245 | }, | ||
246 | |||
247 | function createVideoPreview (callback) { | ||
248 | createPreview(video, videoPath, callback) | ||
249 | } | ||
250 | ) | ||
251 | |||
252 | if (constants.CONFIG.TRANSCODING.ENABLED === true) { | ||
253 | tasks.push( | ||
254 | function createVideoTranscoderJob (callback) { | ||
255 | const dataInput = { | ||
256 | id: video.id | ||
257 | } | ||
258 | |||
259 | jobScheduler.createJob(options.transaction, 'videoTranscoder', dataInput, callback) | ||
260 | } | ||
261 | ) | ||
262 | } | ||
263 | |||
264 | return parallel(tasks, next) | ||
265 | } | ||
266 | |||
267 | return next() | ||
268 | } | ||
269 | |||
270 | function afterDestroy (video, options, next) { | ||
271 | const tasks = [] | ||
272 | |||
273 | tasks.push( | ||
274 | function (callback) { | ||
275 | removeThumbnail(video, callback) | ||
276 | } | ||
277 | ) | ||
278 | |||
279 | if (video.isOwned()) { | ||
280 | tasks.push( | ||
281 | function removeVideoFile (callback) { | ||
282 | removeFile(video, callback) | ||
283 | }, | ||
284 | |||
285 | function removeVideoTorrent (callback) { | ||
286 | removeTorrent(video, callback) | ||
287 | }, | ||
288 | |||
289 | function removeVideoPreview (callback) { | ||
290 | removePreview(video, callback) | ||
291 | }, | ||
292 | |||
293 | function removeVideoToFriends (callback) { | ||
294 | const params = { | ||
295 | remoteId: video.id | ||
296 | } | ||
297 | |||
298 | friends.removeVideoToFriends(params) | ||
299 | |||
300 | return callback() | ||
301 | } | ||
302 | ) | ||
303 | } | ||
304 | |||
305 | parallel(tasks, next) | ||
306 | } | ||
307 | |||
308 | // ------------------------------ METHODS ------------------------------ | ||
309 | |||
310 | function associate (models) { | ||
311 | this.belongsTo(models.Author, { | ||
312 | foreignKey: { | ||
313 | name: 'authorId', | ||
314 | allowNull: false | ||
315 | }, | ||
316 | onDelete: 'cascade' | ||
317 | }) | ||
318 | |||
319 | this.belongsToMany(models.Tag, { | ||
320 | foreignKey: 'videoId', | ||
321 | through: models.VideoTag, | ||
322 | onDelete: 'cascade' | ||
323 | }) | ||
324 | |||
325 | this.hasMany(models.VideoAbuse, { | ||
326 | foreignKey: { | ||
327 | name: 'videoId', | ||
328 | allowNull: false | ||
329 | }, | ||
330 | onDelete: 'cascade' | ||
331 | }) | ||
332 | } | ||
333 | |||
334 | function generateMagnetUri () { | ||
335 | let baseUrlHttp, baseUrlWs | ||
336 | |||
337 | if (this.isOwned()) { | ||
338 | baseUrlHttp = constants.CONFIG.WEBSERVER.URL | ||
339 | baseUrlWs = constants.CONFIG.WEBSERVER.WS + '://' + constants.CONFIG.WEBSERVER.HOSTNAME + ':' + constants.CONFIG.WEBSERVER.PORT | ||
340 | } else { | ||
341 | baseUrlHttp = constants.REMOTE_SCHEME.HTTP + '://' + this.Author.Pod.host | ||
342 | baseUrlWs = constants.REMOTE_SCHEME.WS + '://' + this.Author.Pod.host | ||
343 | } | ||
344 | |||
345 | const xs = baseUrlHttp + constants.STATIC_PATHS.TORRENTS + this.getTorrentName() | ||
346 | const announce = baseUrlWs + '/tracker/socket' | ||
347 | const urlList = [ baseUrlHttp + constants.STATIC_PATHS.WEBSEED + this.getVideoFilename() ] | ||
348 | |||
349 | const magnetHash = { | ||
350 | xs, | ||
351 | announce, | ||
352 | urlList, | ||
353 | infoHash: this.infoHash, | ||
354 | name: this.name | ||
355 | } | ||
356 | |||
357 | return magnetUtil.encode(magnetHash) | ||
358 | } | ||
359 | |||
360 | function getVideoFilename () { | ||
361 | if (this.isOwned()) return this.id + this.extname | ||
362 | |||
363 | return this.remoteId + this.extname | ||
364 | } | ||
365 | |||
366 | function getThumbnailName () { | ||
367 | // We always have a copy of the thumbnail | ||
368 | return this.id + '.jpg' | ||
369 | } | ||
370 | |||
371 | function getPreviewName () { | ||
372 | const extension = '.jpg' | ||
373 | |||
374 | if (this.isOwned()) return this.id + extension | ||
375 | |||
376 | return this.remoteId + extension | ||
377 | } | ||
378 | |||
379 | function getTorrentName () { | ||
380 | const extension = '.torrent' | ||
381 | |||
382 | if (this.isOwned()) return this.id + extension | ||
383 | |||
384 | return this.remoteId + extension | ||
385 | } | ||
386 | |||
387 | function isOwned () { | ||
388 | return this.remoteId === null | ||
389 | } | ||
390 | |||
391 | function toFormatedJSON () { | ||
392 | let podHost | ||
393 | |||
394 | if (this.Author.Pod) { | ||
395 | podHost = this.Author.Pod.host | ||
396 | } else { | ||
397 | // It means it's our video | ||
398 | podHost = constants.CONFIG.WEBSERVER.HOST | ||
399 | } | ||
400 | |||
401 | // Maybe our pod is not up to date and there are new categories since our version | ||
402 | let categoryLabel = constants.VIDEO_CATEGORIES[this.category] | ||
403 | if (!categoryLabel) categoryLabel = 'Misc' | ||
404 | |||
405 | // Maybe our pod is not up to date and there are new licences since our version | ||
406 | let licenceLabel = constants.VIDEO_LICENCES[this.licence] | ||
407 | if (!licenceLabel) licenceLabel = 'Unknown' | ||
408 | |||
409 | // Language is an optional attribute | ||
410 | let languageLabel = constants.VIDEO_LANGUAGES[this.language] | ||
411 | if (!languageLabel) languageLabel = 'Unknown' | ||
412 | |||
413 | const json = { | ||
414 | id: this.id, | ||
415 | name: this.name, | ||
416 | category: this.category, | ||
417 | categoryLabel, | ||
418 | licence: this.licence, | ||
419 | licenceLabel, | ||
420 | language: this.language, | ||
421 | languageLabel, | ||
422 | nsfw: this.nsfw, | ||
423 | description: this.description, | ||
424 | podHost, | ||
425 | isLocal: this.isOwned(), | ||
426 | magnetUri: this.generateMagnetUri(), | ||
427 | author: this.Author.name, | ||
428 | duration: this.duration, | ||
429 | views: this.views, | ||
430 | likes: this.likes, | ||
431 | dislikes: this.dislikes, | ||
432 | tags: map(this.Tags, 'name'), | ||
433 | thumbnailPath: pathUtils.join(constants.STATIC_PATHS.THUMBNAILS, this.getThumbnailName()), | ||
434 | createdAt: this.createdAt, | ||
435 | updatedAt: this.updatedAt | ||
436 | } | ||
437 | |||
438 | return json | ||
439 | } | ||
440 | |||
441 | function toAddRemoteJSON (callback) { | ||
442 | const self = this | ||
443 | |||
444 | // Get thumbnail data to send to the other pod | ||
445 | const thumbnailPath = pathUtils.join(constants.CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) | ||
446 | fs.readFile(thumbnailPath, function (err, thumbnailData) { | ||
447 | if (err) { | ||
448 | logger.error('Cannot read the thumbnail of the video') | ||
449 | return callback(err) | ||
450 | } | ||
451 | |||
452 | const remoteVideo = { | ||
453 | name: self.name, | ||
454 | category: self.category, | ||
455 | licence: self.licence, | ||
456 | language: self.language, | ||
457 | nsfw: self.nsfw, | ||
458 | description: self.description, | ||
459 | infoHash: self.infoHash, | ||
460 | remoteId: self.id, | ||
461 | author: self.Author.name, | ||
462 | duration: self.duration, | ||
463 | thumbnailData: thumbnailData.toString('binary'), | ||
464 | tags: map(self.Tags, 'name'), | ||
465 | createdAt: self.createdAt, | ||
466 | updatedAt: self.updatedAt, | ||
467 | extname: self.extname, | ||
468 | views: self.views, | ||
469 | likes: self.likes, | ||
470 | dislikes: self.dislikes | ||
471 | } | ||
472 | |||
473 | return callback(null, remoteVideo) | ||
474 | }) | ||
475 | } | ||
476 | |||
477 | function toUpdateRemoteJSON (callback) { | ||
478 | const json = { | ||
479 | name: this.name, | ||
480 | category: this.category, | ||
481 | licence: this.licence, | ||
482 | language: this.language, | ||
483 | nsfw: this.nsfw, | ||
484 | description: this.description, | ||
485 | infoHash: this.infoHash, | ||
486 | remoteId: this.id, | ||
487 | author: this.Author.name, | ||
488 | duration: this.duration, | ||
489 | tags: map(this.Tags, 'name'), | ||
490 | createdAt: this.createdAt, | ||
491 | updatedAt: this.updatedAt, | ||
492 | extname: this.extname, | ||
493 | views: this.views, | ||
494 | likes: this.likes, | ||
495 | dislikes: this.dislikes | ||
496 | } | ||
497 | |||
498 | return json | ||
499 | } | ||
500 | |||
501 | function transcodeVideofile (finalCallback) { | ||
502 | const video = this | ||
503 | |||
504 | const videosDirectory = constants.CONFIG.STORAGE.VIDEOS_DIR | ||
505 | const newExtname = '.mp4' | ||
506 | const videoInputPath = pathUtils.join(videosDirectory, video.getVideoFilename()) | ||
507 | const videoOutputPath = pathUtils.join(videosDirectory, video.id + '-transcoded' + newExtname) | ||
508 | |||
509 | ffmpeg(videoInputPath) | ||
510 | .output(videoOutputPath) | ||
511 | .videoCodec('libx264') | ||
512 | .outputOption('-threads ' + constants.CONFIG.TRANSCODING.THREADS) | ||
513 | .outputOption('-movflags faststart') | ||
514 | .on('error', finalCallback) | ||
515 | .on('end', function () { | ||
516 | series([ | ||
517 | function removeOldFile (callback) { | ||
518 | fs.unlink(videoInputPath, callback) | ||
519 | }, | ||
520 | |||
521 | function moveNewFile (callback) { | ||
522 | // Important to do this before getVideoFilename() to take in account the new file extension | ||
523 | video.set('extname', newExtname) | ||
524 | |||
525 | const newVideoPath = pathUtils.join(videosDirectory, video.getVideoFilename()) | ||
526 | fs.rename(videoOutputPath, newVideoPath, callback) | ||
527 | }, | ||
528 | |||
529 | function torrent (callback) { | ||
530 | const newVideoPath = pathUtils.join(videosDirectory, video.getVideoFilename()) | ||
531 | createTorrentFromVideo(video, newVideoPath, callback) | ||
532 | }, | ||
533 | |||
534 | function videoExtension (callback) { | ||
535 | video.save().asCallback(callback) | ||
536 | } | ||
537 | |||
538 | ], function (err) { | ||
539 | if (err) { | ||
540 | // Autodescruction... | ||
541 | video.destroy().asCallback(function (err) { | ||
542 | if (err) logger.error('Cannot destruct video after transcoding failure.', { error: err }) | ||
543 | }) | ||
544 | |||
545 | return finalCallback(err) | ||
546 | } | ||
547 | |||
548 | return finalCallback(null) | ||
549 | }) | ||
550 | }) | ||
551 | .run() | ||
552 | } | ||
553 | |||
554 | // ------------------------------ STATICS ------------------------------ | ||
555 | |||
556 | function generateThumbnailFromData (video, thumbnailData, callback) { | ||
557 | // Creating the thumbnail for a remote video | ||
558 | |||
559 | const thumbnailName = video.getThumbnailName() | ||
560 | const thumbnailPath = pathUtils.join(constants.CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName) | ||
561 | fs.writeFile(thumbnailPath, Buffer.from(thumbnailData, 'binary'), function (err) { | ||
562 | if (err) return callback(err) | ||
563 | |||
564 | return callback(null, thumbnailName) | ||
565 | }) | ||
566 | } | ||
567 | |||
568 | function getDurationFromFile (videoPath, callback) { | ||
569 | ffmpeg.ffprobe(videoPath, function (err, metadata) { | ||
570 | if (err) return callback(err) | ||
571 | |||
572 | return callback(null, Math.floor(metadata.format.duration)) | ||
573 | }) | ||
574 | } | ||
575 | |||
576 | function list (callback) { | ||
577 | return this.findAll().asCallback(callback) | ||
578 | } | ||
579 | |||
580 | function listForApi (start, count, sort, callback) { | ||
581 | // Exclude Blakclisted videos from the list | ||
582 | const query = { | ||
583 | offset: start, | ||
584 | limit: count, | ||
585 | distinct: true, // For the count, a video can have many tags | ||
586 | order: [ modelUtils.getSort(sort), [ this.sequelize.models.Tag, 'name', 'ASC' ] ], | ||
587 | include: [ | ||
588 | { | ||
589 | model: this.sequelize.models.Author, | ||
590 | include: [ { model: this.sequelize.models.Pod, required: false } ] | ||
591 | }, | ||
592 | |||
593 | this.sequelize.models.Tag | ||
594 | ], | ||
595 | where: createBaseVideosWhere.call(this) | ||
596 | } | ||
597 | |||
598 | return this.findAndCountAll(query).asCallback(function (err, result) { | ||
599 | if (err) return callback(err) | ||
600 | |||
601 | return callback(null, result.rows, result.count) | ||
602 | }) | ||
603 | } | ||
604 | |||
605 | function loadByHostAndRemoteId (fromHost, remoteId, callback) { | ||
606 | const query = { | ||
607 | where: { | ||
608 | remoteId: remoteId | ||
609 | }, | ||
610 | include: [ | ||
611 | { | ||
612 | model: this.sequelize.models.Author, | ||
613 | include: [ | ||
614 | { | ||
615 | model: this.sequelize.models.Pod, | ||
616 | required: true, | ||
617 | where: { | ||
618 | host: fromHost | ||
619 | } | ||
620 | } | ||
621 | ] | ||
622 | } | ||
623 | ] | ||
624 | } | ||
625 | |||
626 | return this.findOne(query).asCallback(callback) | ||
627 | } | ||
628 | |||
629 | function listOwnedAndPopulateAuthorAndTags (callback) { | ||
630 | // If remoteId is null this is *our* video | ||
631 | const query = { | ||
632 | where: { | ||
633 | remoteId: null | ||
634 | }, | ||
635 | include: [ this.sequelize.models.Author, this.sequelize.models.Tag ] | ||
636 | } | ||
637 | |||
638 | return this.findAll(query).asCallback(callback) | ||
639 | } | ||
640 | |||
641 | function listOwnedByAuthor (author, callback) { | ||
642 | const query = { | ||
643 | where: { | ||
644 | remoteId: null | ||
645 | }, | ||
646 | include: [ | ||
647 | { | ||
648 | model: this.sequelize.models.Author, | ||
649 | where: { | ||
650 | name: author | ||
651 | } | ||
652 | } | ||
653 | ] | ||
654 | } | ||
655 | |||
656 | return this.findAll(query).asCallback(callback) | ||
657 | } | ||
658 | |||
659 | function load (id, callback) { | ||
660 | return this.findById(id).asCallback(callback) | ||
661 | } | ||
662 | |||
663 | function loadAndPopulateAuthor (id, callback) { | ||
664 | const options = { | ||
665 | include: [ this.sequelize.models.Author ] | ||
666 | } | ||
667 | |||
668 | return this.findById(id, options).asCallback(callback) | ||
669 | } | ||
670 | |||
671 | function loadAndPopulateAuthorAndPodAndTags (id, callback) { | ||
672 | const options = { | ||
673 | include: [ | ||
674 | { | ||
675 | model: this.sequelize.models.Author, | ||
676 | include: [ { model: this.sequelize.models.Pod, required: false } ] | ||
677 | }, | ||
678 | this.sequelize.models.Tag | ||
679 | ] | ||
680 | } | ||
681 | |||
682 | return this.findById(id, options).asCallback(callback) | ||
683 | } | ||
684 | |||
685 | function searchAndPopulateAuthorAndPodAndTags (value, field, start, count, sort, callback) { | ||
686 | const podInclude = { | ||
687 | model: this.sequelize.models.Pod, | ||
688 | required: false | ||
689 | } | ||
690 | |||
691 | const authorInclude = { | ||
692 | model: this.sequelize.models.Author, | ||
693 | include: [ | ||
694 | podInclude | ||
695 | ] | ||
696 | } | ||
697 | |||
698 | const tagInclude = { | ||
699 | model: this.sequelize.models.Tag | ||
700 | } | ||
701 | |||
702 | const query = { | ||
703 | where: createBaseVideosWhere.call(this), | ||
704 | offset: start, | ||
705 | limit: count, | ||
706 | distinct: true, // For the count, a video can have many tags | ||
707 | order: [ modelUtils.getSort(sort), [ this.sequelize.models.Tag, 'name', 'ASC' ] ] | ||
708 | } | ||
709 | |||
710 | // Make an exact search with the magnet | ||
711 | if (field === 'magnetUri') { | ||
712 | const infoHash = magnetUtil.decode(value).infoHash | ||
713 | query.where.infoHash = infoHash | ||
714 | } else if (field === 'tags') { | ||
715 | const escapedValue = this.sequelize.escape('%' + value + '%') | ||
716 | query.where.id.$in = this.sequelize.literal( | ||
717 | '(SELECT "VideoTags"."videoId" FROM "Tags" INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId" WHERE name LIKE ' + escapedValue + ')' | ||
718 | ) | ||
719 | } else if (field === 'host') { | ||
720 | // FIXME: Include our pod? (not stored in the database) | ||
721 | podInclude.where = { | ||
722 | host: { | ||
723 | $like: '%' + value + '%' | ||
724 | } | ||
725 | } | ||
726 | podInclude.required = true | ||
727 | } else if (field === 'author') { | ||
728 | authorInclude.where = { | ||
729 | name: { | ||
730 | $like: '%' + value + '%' | ||
731 | } | ||
732 | } | ||
733 | |||
734 | // authorInclude.or = true | ||
735 | } else { | ||
736 | query.where[field] = { | ||
737 | $like: '%' + value + '%' | ||
738 | } | ||
739 | } | ||
740 | |||
741 | query.include = [ | ||
742 | authorInclude, tagInclude | ||
743 | ] | ||
744 | |||
745 | if (tagInclude.where) { | ||
746 | // query.include.push([ this.sequelize.models.Tag ]) | ||
747 | } | ||
748 | |||
749 | return this.findAndCountAll(query).asCallback(function (err, result) { | ||
750 | if (err) return callback(err) | ||
751 | |||
752 | return callback(null, result.rows, result.count) | ||
753 | }) | ||
754 | } | ||
755 | |||
756 | // --------------------------------------------------------------------------- | ||
757 | |||
758 | function createBaseVideosWhere () { | ||
759 | return { | ||
760 | id: { | ||
761 | $notIn: this.sequelize.literal( | ||
762 | '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")' | ||
763 | ) | ||
764 | } | ||
765 | } | ||
766 | } | ||
767 | |||
768 | function removeThumbnail (video, callback) { | ||
769 | const thumbnailPath = pathUtils.join(constants.CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName()) | ||
770 | fs.unlink(thumbnailPath, callback) | ||
771 | } | ||
772 | |||
773 | function removeFile (video, callback) { | ||
774 | const filePath = pathUtils.join(constants.CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename()) | ||
775 | fs.unlink(filePath, callback) | ||
776 | } | ||
777 | |||
778 | function removeTorrent (video, callback) { | ||
779 | const torrenPath = pathUtils.join(constants.CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName()) | ||
780 | fs.unlink(torrenPath, callback) | ||
781 | } | ||
782 | |||
783 | function removePreview (video, callback) { | ||
784 | // Same name than video thumnail | ||
785 | fs.unlink(constants.CONFIG.STORAGE.PREVIEWS_DIR + video.getPreviewName(), callback) | ||
786 | } | ||
787 | |||
788 | function createTorrentFromVideo (video, videoPath, callback) { | ||
789 | const options = { | ||
790 | announceList: [ | ||
791 | [ constants.CONFIG.WEBSERVER.WS + '://' + constants.CONFIG.WEBSERVER.HOSTNAME + ':' + constants.CONFIG.WEBSERVER.PORT + '/tracker/socket' ] | ||
792 | ], | ||
793 | urlList: [ | ||
794 | constants.CONFIG.WEBSERVER.URL + constants.STATIC_PATHS.WEBSEED + video.getVideoFilename() | ||
795 | ] | ||
796 | } | ||
797 | |||
798 | createTorrent(videoPath, options, function (err, torrent) { | ||
799 | if (err) return callback(err) | ||
800 | |||
801 | const filePath = pathUtils.join(constants.CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName()) | ||
802 | fs.writeFile(filePath, torrent, function (err) { | ||
803 | if (err) return callback(err) | ||
804 | |||
805 | const parsedTorrent = parseTorrent(torrent) | ||
806 | video.set('infoHash', parsedTorrent.infoHash) | ||
807 | video.validate().asCallback(callback) | ||
808 | }) | ||
809 | }) | ||
810 | } | ||
811 | |||
812 | function createPreview (video, videoPath, callback) { | ||
813 | generateImage(video, videoPath, constants.CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName(), callback) | ||
814 | } | ||
815 | |||
816 | function createThumbnail (video, videoPath, callback) { | ||
817 | generateImage(video, videoPath, constants.CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName(), constants.THUMBNAILS_SIZE, callback) | ||
818 | } | ||
819 | |||
820 | function generateImage (video, videoPath, folder, imageName, size, callback) { | ||
821 | const options = { | ||
822 | filename: imageName, | ||
823 | count: 1, | ||
824 | folder | ||
825 | } | ||
826 | |||
827 | if (!callback) { | ||
828 | callback = size | ||
829 | } else { | ||
830 | options.size = size | ||
831 | } | ||
832 | |||
833 | ffmpeg(videoPath) | ||
834 | .on('error', callback) | ||
835 | .on('end', function () { | ||
836 | callback(null, imageName) | ||
837 | }) | ||
838 | .thumbnail(options) | ||
839 | } | ||
840 | |||
841 | function removeFromBlacklist (video, callback) { | ||
842 | // Find the blacklisted video | ||
843 | db.BlacklistedVideo.loadByVideoId(video.id, function (err, video) { | ||
844 | // If an error occured, stop here | ||
845 | if (err) { | ||
846 | logger.error('Error when fetching video from blacklist.', { error: err }) | ||
847 | return callback(err) | ||
848 | } | ||
849 | |||
850 | // If we found the video, remove it from the blacklist | ||
851 | if (video) { | ||
852 | video.destroy().asCallback(callback) | ||
853 | } else { | ||
854 | // If haven't found it, simply ignore it and do nothing | ||
855 | return callback() | ||
856 | } | ||
857 | }) | ||
858 | } | ||