]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/video/video.ts
b7eb24c4a0f38559e25c49c280f569e1236506e8
[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 previewPath: join(STATIC_PATHS.PREVIEWS, this.getPreviewName()),
455 createdAt: this.createdAt,
456 updatedAt: this.updatedAt
457 }
458
459 return json
460 }
461
462 toAddRemoteJSON = function (this: VideoInstance) {
463 // Get thumbnail data to send to the other pod
464 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
465
466 return readFileBufferPromise(thumbnailPath).then(thumbnailData => {
467 const remoteVideo = {
468 uuid: this.uuid,
469 name: this.name,
470 category: this.category,
471 licence: this.licence,
472 language: this.language,
473 nsfw: this.nsfw,
474 description: this.description,
475 infoHash: this.infoHash,
476 author: this.Author.name,
477 duration: this.duration,
478 thumbnailData: thumbnailData.toString('binary'),
479 tags: map<TagInstance, string>(this.Tags, 'name'),
480 createdAt: this.createdAt,
481 updatedAt: this.updatedAt,
482 extname: this.extname,
483 views: this.views,
484 likes: this.likes,
485 dislikes: this.dislikes
486 }
487
488 return remoteVideo
489 })
490 }
491
492 toUpdateRemoteJSON = function (this: VideoInstance) {
493 const json = {
494 uuid: this.uuid,
495 name: this.name,
496 category: this.category,
497 licence: this.licence,
498 language: this.language,
499 nsfw: this.nsfw,
500 description: this.description,
501 infoHash: this.infoHash,
502 author: this.Author.name,
503 duration: this.duration,
504 tags: map<TagInstance, string>(this.Tags, 'name'),
505 createdAt: this.createdAt,
506 updatedAt: this.updatedAt,
507 extname: this.extname,
508 views: this.views,
509 likes: this.likes,
510 dislikes: this.dislikes
511 }
512
513 return json
514 }
515
516 transcodeVideofile = function (this: VideoInstance) {
517 const video = this
518
519 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
520 const newExtname = '.mp4'
521 const videoInputPath = join(videosDirectory, video.getVideoFilename())
522 const videoOutputPath = join(videosDirectory, video.id + '-transcoded' + newExtname)
523
524 return new Promise<void>((res, rej) => {
525 ffmpeg(videoInputPath)
526 .output(videoOutputPath)
527 .videoCodec('libx264')
528 .outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
529 .outputOption('-movflags faststart')
530 .on('error', rej)
531 .on('end', () => {
532
533 return unlinkPromise(videoInputPath)
534 .then(() => {
535 // Important to do this before getVideoFilename() to take in account the new file extension
536 video.set('extname', newExtname)
537
538 const newVideoPath = join(videosDirectory, video.getVideoFilename())
539 return renamePromise(videoOutputPath, newVideoPath)
540 })
541 .then(() => {
542 const newVideoPath = join(videosDirectory, video.getVideoFilename())
543 return createTorrentFromVideo(video, newVideoPath)
544 })
545 .then(() => {
546 return video.save()
547 })
548 .then(() => {
549 return res()
550 })
551 .catch(err => {
552 // Autodesctruction...
553 video.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
554
555 return rej(err)
556 })
557 })
558 .run()
559 })
560 }
561
562 // ------------------------------ STATICS ------------------------------
563
564 generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) {
565 // Creating the thumbnail for a remote video
566
567 const thumbnailName = video.getThumbnailName()
568 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
569 return writeFilePromise(thumbnailPath, Buffer.from(thumbnailData, 'binary')).then(() => {
570 return thumbnailName
571 })
572 }
573
574 getDurationFromFile = function (videoPath: string) {
575 return new Promise<number>((res, rej) => {
576 ffmpeg.ffprobe(videoPath, (err, metadata) => {
577 if (err) return rej(err)
578
579 return res(Math.floor(metadata.format.duration))
580 })
581 })
582 }
583
584 list = function () {
585 return Video.findAll()
586 }
587
588 listForApi = function (start: number, count: number, sort: string) {
589 // Exclude Blakclisted videos from the list
590 const query = {
591 distinct: true,
592 offset: start,
593 limit: count,
594 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
595 include: [
596 {
597 model: Video['sequelize'].models.Author,
598 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
599 },
600
601 Video['sequelize'].models.Tag
602 ],
603 where: createBaseVideosWhere()
604 }
605
606 return Video.findAndCountAll(query).then(({ rows, count }) => {
607 return {
608 data: rows,
609 total: count
610 }
611 })
612 }
613
614 loadByHostAndUUID = function (fromHost: string, uuid: string) {
615 const query = {
616 where: {
617 uuid
618 },
619 include: [
620 {
621 model: Video['sequelize'].models.Author,
622 include: [
623 {
624 model: Video['sequelize'].models.Pod,
625 required: true,
626 where: {
627 host: fromHost
628 }
629 }
630 ]
631 }
632 ]
633 }
634
635 return Video.findOne(query)
636 }
637
638 listOwnedAndPopulateAuthorAndTags = function () {
639 const query = {
640 where: {
641 remote: false
642 },
643 include: [ Video['sequelize'].models.Author, Video['sequelize'].models.Tag ]
644 }
645
646 return Video.findAll(query)
647 }
648
649 listOwnedByAuthor = function (author: string) {
650 const query = {
651 where: {
652 remote: false
653 },
654 include: [
655 {
656 model: Video['sequelize'].models.Author,
657 where: {
658 name: author
659 }
660 }
661 ]
662 }
663
664 return Video.findAll(query)
665 }
666
667 load = function (id: number) {
668 return Video.findById(id)
669 }
670
671 loadByUUID = function (uuid: string) {
672 const query = {
673 where: {
674 uuid
675 }
676 }
677 return Video.findOne(query)
678 }
679
680 loadAndPopulateAuthor = function (id: number) {
681 const options = {
682 include: [ Video['sequelize'].models.Author ]
683 }
684
685 return Video.findById(id, options)
686 }
687
688 loadAndPopulateAuthorAndPodAndTags = function (id: number) {
689 const options = {
690 include: [
691 {
692 model: Video['sequelize'].models.Author,
693 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
694 },
695 Video['sequelize'].models.Tag
696 ]
697 }
698
699 return Video.findById(id, options)
700 }
701
702 loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) {
703 const options = {
704 where: {
705 uuid
706 },
707 include: [
708 {
709 model: Video['sequelize'].models.Author,
710 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
711 },
712 Video['sequelize'].models.Tag
713 ]
714 }
715
716 return Video.findOne(options)
717 }
718
719 searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, start: number, count: number, sort: string) {
720 const podInclude: Sequelize.IncludeOptions = {
721 model: Video['sequelize'].models.Pod,
722 required: false
723 }
724
725 const authorInclude: Sequelize.IncludeOptions = {
726 model: Video['sequelize'].models.Author,
727 include: [
728 podInclude
729 ]
730 }
731
732 const tagInclude: Sequelize.IncludeOptions = {
733 model: Video['sequelize'].models.Tag
734 }
735
736 const query: Sequelize.FindOptions = {
737 distinct: true,
738 where: createBaseVideosWhere(),
739 offset: start,
740 limit: count,
741 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ]
742 }
743
744 // Make an exact search with the magnet
745 if (field === 'magnetUri') {
746 const infoHash = magnetUtil.decode(value).infoHash
747 query.where['infoHash'] = infoHash
748 } else if (field === 'tags') {
749 const escapedValue = Video['sequelize'].escape('%' + value + '%')
750 query.where['id'].$in = Video['sequelize'].literal(
751 `(SELECT "VideoTags"."videoId"
752 FROM "Tags"
753 INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId"
754 WHERE name ILIKE ${escapedValue}
755 )`
756 )
757 } else if (field === 'host') {
758 // FIXME: Include our pod? (not stored in the database)
759 podInclude.where = {
760 host: {
761 $iLike: '%' + value + '%'
762 }
763 }
764 podInclude.required = true
765 } else if (field === 'author') {
766 authorInclude.where = {
767 name: {
768 $iLike: '%' + value + '%'
769 }
770 }
771
772 // authorInclude.or = true
773 } else {
774 query.where[field] = {
775 $iLike: '%' + value + '%'
776 }
777 }
778
779 query.include = [
780 authorInclude, tagInclude
781 ]
782
783 return Video.findAndCountAll(query).then(({ rows, count }) => {
784 return {
785 data: rows,
786 total: count
787 }
788 })
789 }
790
791 // ---------------------------------------------------------------------------
792
793 function createBaseVideosWhere () {
794 return {
795 id: {
796 $notIn: Video['sequelize'].literal(
797 '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")'
798 )
799 }
800 }
801 }
802
803 function removeThumbnail (video: VideoInstance) {
804 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName())
805 return unlinkPromise(thumbnailPath)
806 }
807
808 function removeFile (video: VideoInstance) {
809 const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename())
810 return unlinkPromise(filePath)
811 }
812
813 function removeTorrent (video: VideoInstance) {
814 const torrenPath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName())
815 return unlinkPromise(torrenPath)
816 }
817
818 function removePreview (video: VideoInstance) {
819 // Same name than video thumnail
820 return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + video.getPreviewName())
821 }
822
823 function createTorrentFromVideo (video: VideoInstance, videoPath: string) {
824 const options = {
825 announceList: [
826 [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
827 ],
828 urlList: [
829 CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + video.getVideoFilename()
830 ]
831 }
832
833 return createTorrentPromise(videoPath, options)
834 .then(torrent => {
835 const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName())
836 return writeFilePromise(filePath, torrent).then(() => torrent)
837 })
838 .then(torrent => {
839 const parsedTorrent = parseTorrent(torrent)
840 video.set('infoHash', parsedTorrent.infoHash)
841 return video.validate()
842 })
843 }
844
845 function createPreview (video: VideoInstance, videoPath: string) {
846 return generateImage(video, videoPath, CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName(), null)
847 }
848
849 function createThumbnail (video: VideoInstance, videoPath: string) {
850 return generateImage(video, videoPath, CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName(), THUMBNAILS_SIZE)
851 }
852
853 function generateImage (video: VideoInstance, videoPath: string, folder: string, imageName: string, size: string) {
854 const options = {
855 filename: imageName,
856 count: 1,
857 folder
858 }
859
860 if (size) {
861 options['size'] = size
862 }
863
864 return new Promise<string>((res, rej) => {
865 ffmpeg(videoPath)
866 .on('error', rej)
867 .on('end', () => res(imageName))
868 .thumbnail(options)
869 })
870 }
871
872 function removeFromBlacklist (video: VideoInstance) {
873 // Find the blacklisted video
874 return db.BlacklistedVideo.loadByVideoId(video.id).then(video => {
875 // Not found the video, skip
876 if (!video) {
877 return null
878 }
879
880 // If we found the video, remove it from the blacklist
881 return video.destroy()
882 })
883 }