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