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