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