]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/video/video.ts
Use global uuid instead of remoteId for videos
[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: function (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: function (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: function (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: function (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: function (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: function (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: function (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: function (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().asCallback(function (err) {
553 if (err) logger.error('Cannot destruct video after transcoding failure.', err)
554 })
555
556 return rej(err)
557 })
558 })
559 .run()
560 })
561 }
562
563 // ------------------------------ STATICS ------------------------------
564
565 generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) {
566 // Creating the thumbnail for a remote video
567
568 const thumbnailName = video.getThumbnailName()
569 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
570 return writeFilePromise(thumbnailPath, Buffer.from(thumbnailData, 'binary')).then(() => {
571 return thumbnailName
572 })
573 }
574
575 getDurationFromFile = function (videoPath: string) {
576 return new Promise<number>((res, rej) => {
577 ffmpeg.ffprobe(videoPath, function (err, metadata) {
578 if (err) return rej(err)
579
580 return res(Math.floor(metadata.format.duration))
581 })
582 })
583 }
584
585 list = function () {
586 return Video.findAll()
587 }
588
589 listForApi = function (start: number, count: number, sort: string) {
590 // Exclude Blakclisted videos from the list
591 const query = {
592 distinct: true,
593 offset: start,
594 limit: count,
595 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
596 include: [
597 {
598 model: Video['sequelize'].models.Author,
599 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
600 },
601
602 Video['sequelize'].models.Tag
603 ],
604 where: createBaseVideosWhere()
605 }
606
607 return Video.findAndCountAll(query).then(({ rows, count }) => {
608 return {
609 data: rows,
610 total: count
611 }
612 })
613 }
614
615 loadByHostAndUUID = function (fromHost: string, uuid: string) {
616 const query = {
617 where: {
618 uuid
619 },
620 include: [
621 {
622 model: Video['sequelize'].models.Author,
623 include: [
624 {
625 model: Video['sequelize'].models.Pod,
626 required: true,
627 where: {
628 host: fromHost
629 }
630 }
631 ]
632 }
633 ]
634 }
635
636 return Video.findOne(query)
637 }
638
639 listOwnedAndPopulateAuthorAndTags = function () {
640 const query = {
641 where: {
642 remote: false
643 },
644 include: [ Video['sequelize'].models.Author, Video['sequelize'].models.Tag ]
645 }
646
647 return Video.findAll(query)
648 }
649
650 listOwnedByAuthor = function (author: string) {
651 const query = {
652 where: {
653 remote: false
654 },
655 include: [
656 {
657 model: Video['sequelize'].models.Author,
658 where: {
659 name: author
660 }
661 }
662 ]
663 }
664
665 return Video.findAll(query)
666 }
667
668 load = function (id: number) {
669 return Video.findById(id)
670 }
671
672 loadByUUID = function (uuid: string) {
673 const query = {
674 where: {
675 uuid
676 }
677 }
678 return Video.findOne(query)
679 }
680
681 loadAndPopulateAuthor = function (id: number) {
682 const options = {
683 include: [ Video['sequelize'].models.Author ]
684 }
685
686 return Video.findById(id, options)
687 }
688
689 loadAndPopulateAuthorAndPodAndTags = function (id: number) {
690 const options = {
691 include: [
692 {
693 model: Video['sequelize'].models.Author,
694 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
695 },
696 Video['sequelize'].models.Tag
697 ]
698 }
699
700 return Video.findById(id, options)
701 }
702
703 loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) {
704 const options = {
705 where: {
706 uuid
707 },
708 include: [
709 {
710 model: Video['sequelize'].models.Author,
711 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
712 },
713 Video['sequelize'].models.Tag
714 ]
715 }
716
717 return Video.findOne(options)
718 }
719
720 searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, start: number, count: number, sort: string) {
721 const podInclude: Sequelize.IncludeOptions = {
722 model: Video['sequelize'].models.Pod,
723 required: false
724 }
725
726 const authorInclude: Sequelize.IncludeOptions = {
727 model: Video['sequelize'].models.Author,
728 include: [
729 podInclude
730 ]
731 }
732
733 const tagInclude: Sequelize.IncludeOptions = {
734 model: Video['sequelize'].models.Tag
735 }
736
737 const query: Sequelize.FindOptions = {
738 distinct: true,
739 where: createBaseVideosWhere(),
740 offset: start,
741 limit: count,
742 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ]
743 }
744
745 // Make an exact search with the magnet
746 if (field === 'magnetUri') {
747 const infoHash = magnetUtil.decode(value).infoHash
748 query.where['infoHash'] = infoHash
749 } else if (field === 'tags') {
750 const escapedValue = Video['sequelize'].escape('%' + value + '%')
751 query.where['id'].$in = Video['sequelize'].literal(
752 `(SELECT "VideoTags"."videoId"
753 FROM "Tags"
754 INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId"
755 WHERE name ILIKE ${escapedValue}
756 )`
757 )
758 } else if (field === 'host') {
759 // FIXME: Include our pod? (not stored in the database)
760 podInclude.where = {
761 host: {
762 $iLike: '%' + value + '%'
763 }
764 }
765 podInclude.required = true
766 } else if (field === 'author') {
767 authorInclude.where = {
768 name: {
769 $iLike: '%' + value + '%'
770 }
771 }
772
773 // authorInclude.or = true
774 } else {
775 query.where[field] = {
776 $iLike: '%' + value + '%'
777 }
778 }
779
780 query.include = [
781 authorInclude, tagInclude
782 ]
783
784 return Video.findAndCountAll(query).then(({ rows, count }) => {
785 return {
786 data: rows,
787 total: count
788 }
789 })
790 }
791
792 // ---------------------------------------------------------------------------
793
794 function createBaseVideosWhere () {
795 return {
796 id: {
797 $notIn: Video['sequelize'].literal(
798 '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")'
799 )
800 }
801 }
802 }
803
804 function removeThumbnail (video: VideoInstance) {
805 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName())
806 return unlinkPromise(thumbnailPath)
807 }
808
809 function removeFile (video: VideoInstance) {
810 const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename())
811 return unlinkPromise(filePath)
812 }
813
814 function removeTorrent (video: VideoInstance) {
815 const torrenPath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName())
816 return unlinkPromise(torrenPath)
817 }
818
819 function removePreview (video: VideoInstance) {
820 // Same name than video thumnail
821 return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + video.getPreviewName())
822 }
823
824 function createTorrentFromVideo (video: VideoInstance, videoPath: string) {
825 const options = {
826 announceList: [
827 [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
828 ],
829 urlList: [
830 CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + video.getVideoFilename()
831 ]
832 }
833
834 return createTorrentPromise(videoPath, options)
835 .then(torrent => {
836 const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName())
837 return writeFilePromise(filePath, torrent).then(() => torrent)
838 })
839 .then(torrent => {
840 const parsedTorrent = parseTorrent(torrent)
841 video.set('infoHash', parsedTorrent.infoHash)
842 return video.validate()
843 })
844 }
845
846 function createPreview (video: VideoInstance, videoPath: string) {
847 return generateImage(video, videoPath, CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName(), null)
848 }
849
850 function createThumbnail (video: VideoInstance, videoPath: string) {
851 return generateImage(video, videoPath, CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName(), THUMBNAILS_SIZE)
852 }
853
854 function generateImage (video: VideoInstance, videoPath: string, folder: string, imageName: string, size: string) {
855 const options = {
856 filename: imageName,
857 count: 1,
858 folder
859 }
860
861 if (size) {
862 options['size'] = size
863 }
864
865 return new Promise<string>((res, rej) => {
866 ffmpeg(videoPath)
867 .on('error', rej)
868 .on('end', function () {
869 return res(imageName)
870 })
871 .thumbnail(options)
872 })
873 }
874
875 function removeFromBlacklist (video: VideoInstance) {
876 // Find the blacklisted video
877 return db.BlacklistedVideo.loadByVideoId(video.id).then(video => {
878 // Not found the video, skip
879 if (!video) {
880 return null
881 }
882
883 // If we found the video, remove it from the blacklist
884 return video.destroy()
885 })
886 }