]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/video/video.ts
Fix lint
[github/Chocobozzz/PeerTube.git] / server / models / video / video.ts
1 import * as safeBuffer from 'safe-buffer'
2 const Buffer = safeBuffer.Buffer
3 import * as magnetUtil from 'magnet-uri'
4 import { map } from 'lodash'
5 import * as parseTorrent from 'parse-torrent'
6 import { join } from 'path'
7 import * as Sequelize from 'sequelize'
8 import * as Promise from 'bluebird'
9 import { maxBy } from 'lodash'
10
11 import { TagInstance } from './tag-interface'
12 import {
13 logger,
14 isVideoNameValid,
15 isVideoCategoryValid,
16 isVideoLicenceValid,
17 isVideoLanguageValid,
18 isVideoNSFWValid,
19 isVideoDescriptionValid,
20 isVideoDurationValid,
21 readFileBufferPromise,
22 unlinkPromise,
23 renamePromise,
24 writeFilePromise,
25 createTorrentPromise,
26 statPromise,
27 generateImageFromVideoFile,
28 transcode,
29 getVideoFileHeight
30 } from '../../helpers'
31 import {
32 CONFIG,
33 REMOTE_SCHEME,
34 STATIC_PATHS,
35 VIDEO_CATEGORIES,
36 VIDEO_LICENCES,
37 VIDEO_LANGUAGES,
38 THUMBNAILS_SIZE
39 } from '../../initializers'
40 import { removeVideoToFriends } from '../../lib'
41 import { VideoResolution } from '../../../shared'
42 import { VideoFileInstance, VideoFileModel } from './video-file-interface'
43
44 import { addMethodsToModel, getSort } from '../utils'
45 import {
46 VideoInstance,
47 VideoAttributes,
48
49 VideoMethods
50 } from './video-interface'
51 import { PREVIEWS_SIZE } from '../../initializers/constants'
52
53 let Video: Sequelize.Model<VideoInstance, VideoAttributes>
54 let getOriginalFile: VideoMethods.GetOriginalFile
55 let getVideoFilename: VideoMethods.GetVideoFilename
56 let getThumbnailName: VideoMethods.GetThumbnailName
57 let getThumbnailPath: VideoMethods.GetThumbnailPath
58 let getPreviewName: VideoMethods.GetPreviewName
59 let getPreviewPath: VideoMethods.GetPreviewPath
60 let getTorrentFileName: VideoMethods.GetTorrentFileName
61 let isOwned: VideoMethods.IsOwned
62 let toFormattedJSON: VideoMethods.ToFormattedJSON
63 let toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON
64 let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
65 let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
66 let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
67 let transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
68 let createPreview: VideoMethods.CreatePreview
69 let createThumbnail: VideoMethods.CreateThumbnail
70 let getVideoFilePath: VideoMethods.GetVideoFilePath
71 let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
72 let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
73 let getEmbedPath: VideoMethods.GetEmbedPath
74
75 let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
76 let list: VideoMethods.List
77 let listForApi: VideoMethods.ListForApi
78 let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
79 let listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags
80 let listOwnedByAuthor: VideoMethods.ListOwnedByAuthor
81 let load: VideoMethods.Load
82 let loadByUUID: VideoMethods.LoadByUUID
83 let loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor
84 let loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags
85 let loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags
86 let searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags
87 let removeThumbnail: VideoMethods.RemoveThumbnail
88 let removePreview: VideoMethods.RemovePreview
89 let removeFile: VideoMethods.RemoveFile
90 let removeTorrent: VideoMethods.RemoveTorrent
91
92 export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
93 Video = sequelize.define<VideoInstance, VideoAttributes>('Video',
94 {
95 uuid: {
96 type: DataTypes.UUID,
97 defaultValue: DataTypes.UUIDV4,
98 allowNull: false,
99 validate: {
100 isUUID: 4
101 }
102 },
103 name: {
104 type: DataTypes.STRING,
105 allowNull: false,
106 validate: {
107 nameValid: value => {
108 const res = isVideoNameValid(value)
109 if (res === false) throw new Error('Video name is not valid.')
110 }
111 }
112 },
113 category: {
114 type: DataTypes.INTEGER,
115 allowNull: false,
116 validate: {
117 categoryValid: value => {
118 const res = isVideoCategoryValid(value)
119 if (res === false) throw new Error('Video category is not valid.')
120 }
121 }
122 },
123 licence: {
124 type: DataTypes.INTEGER,
125 allowNull: false,
126 defaultValue: null,
127 validate: {
128 licenceValid: value => {
129 const res = isVideoLicenceValid(value)
130 if (res === false) throw new Error('Video licence is not valid.')
131 }
132 }
133 },
134 language: {
135 type: DataTypes.INTEGER,
136 allowNull: true,
137 validate: {
138 languageValid: value => {
139 const res = isVideoLanguageValid(value)
140 if (res === false) throw new Error('Video language is not valid.')
141 }
142 }
143 },
144 nsfw: {
145 type: DataTypes.BOOLEAN,
146 allowNull: false,
147 validate: {
148 nsfwValid: value => {
149 const res = isVideoNSFWValid(value)
150 if (res === false) throw new Error('Video nsfw attribute is not valid.')
151 }
152 }
153 },
154 description: {
155 type: DataTypes.STRING,
156 allowNull: false,
157 validate: {
158 descriptionValid: value => {
159 const res = isVideoDescriptionValid(value)
160 if (res === false) throw new Error('Video description is not valid.')
161 }
162 }
163 },
164 duration: {
165 type: DataTypes.INTEGER,
166 allowNull: false,
167 validate: {
168 durationValid: 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 remote: {
202 type: DataTypes.BOOLEAN,
203 allowNull: false,
204 defaultValue: false
205 }
206 },
207 {
208 indexes: [
209 {
210 fields: [ 'name' ]
211 },
212 {
213 fields: [ 'createdAt' ]
214 },
215 {
216 fields: [ 'duration' ]
217 },
218 {
219 fields: [ 'views' ]
220 },
221 {
222 fields: [ 'likes' ]
223 },
224 {
225 fields: [ 'uuid' ]
226 },
227 {
228 fields: [ 'channelId' ]
229 }
230 ],
231 hooks: {
232 afterDestroy
233 }
234 }
235 )
236
237 const classMethods = [
238 associate,
239
240 generateThumbnailFromData,
241 list,
242 listForApi,
243 listOwnedAndPopulateAuthorAndTags,
244 listOwnedByAuthor,
245 load,
246 loadAndPopulateAuthor,
247 loadAndPopulateAuthorAndPodAndTags,
248 loadByHostAndUUID,
249 loadByUUID,
250 loadByUUIDAndPopulateAuthorAndPodAndTags,
251 searchAndPopulateAuthorAndPodAndTags
252 ]
253 const instanceMethods = [
254 createPreview,
255 createThumbnail,
256 createTorrentAndSetInfoHash,
257 getPreviewName,
258 getPreviewPath,
259 getThumbnailName,
260 getThumbnailPath,
261 getTorrentFileName,
262 getVideoFilename,
263 getVideoFilePath,
264 getOriginalFile,
265 isOwned,
266 removeFile,
267 removePreview,
268 removeThumbnail,
269 removeTorrent,
270 toAddRemoteJSON,
271 toFormattedJSON,
272 toFormattedDetailsJSON,
273 toUpdateRemoteJSON,
274 optimizeOriginalVideofile,
275 transcodeOriginalVideofile,
276 getOriginalFileHeight,
277 getEmbedPath
278 ]
279 addMethodsToModel(Video, classMethods, instanceMethods)
280
281 return Video
282 }
283
284 // ------------------------------ METHODS ------------------------------
285
286 function associate (models) {
287 Video.belongsTo(models.VideoChannel, {
288 foreignKey: {
289 name: 'channelId',
290 allowNull: false
291 },
292 onDelete: 'cascade'
293 })
294
295 Video.belongsToMany(models.Tag, {
296 foreignKey: 'videoId',
297 through: models.VideoTag,
298 onDelete: 'cascade'
299 })
300
301 Video.hasMany(models.VideoAbuse, {
302 foreignKey: {
303 name: 'videoId',
304 allowNull: false
305 },
306 onDelete: 'cascade'
307 })
308
309 Video.hasMany(models.VideoFile, {
310 foreignKey: {
311 name: 'videoId',
312 allowNull: false
313 },
314 onDelete: 'cascade'
315 })
316 }
317
318 function afterDestroy (video: VideoInstance, options: { transaction: Sequelize.Transaction }) {
319 const tasks = []
320
321 tasks.push(
322 video.removeThumbnail()
323 )
324
325 if (video.isOwned()) {
326 const removeVideoToFriendsParams = {
327 uuid: video.uuid
328 }
329
330 tasks.push(
331 video.removePreview(),
332 removeVideoToFriends(removeVideoToFriendsParams, options.transaction)
333 )
334
335 // Remove physical files and torrents
336 video.VideoFiles.forEach(file => {
337 tasks.push(video.removeFile(file))
338 tasks.push(video.removeTorrent(file))
339 })
340 }
341
342 return Promise.all(tasks)
343 .catch(err => {
344 logger.error('Some errors when removing files of video %d in after destroy hook.', video.uuid, err)
345 })
346 }
347
348 getOriginalFile = function (this: VideoInstance) {
349 if (Array.isArray(this.VideoFiles) === false) return undefined
350
351 // The original file is the file that have the higher resolution
352 return maxBy(this.VideoFiles, file => file.resolution)
353 }
354
355 getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) {
356 return this.uuid + '-' + videoFile.resolution + videoFile.extname
357 }
358
359 getThumbnailName = function (this: VideoInstance) {
360 // We always have a copy of the thumbnail
361 const extension = '.jpg'
362 return this.uuid + extension
363 }
364
365 getPreviewName = function (this: VideoInstance) {
366 const extension = '.jpg'
367 return this.uuid + extension
368 }
369
370 getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) {
371 const extension = '.torrent'
372 return this.uuid + '-' + videoFile.resolution + extension
373 }
374
375 isOwned = function (this: VideoInstance) {
376 return this.remote === false
377 }
378
379 createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) {
380 const imageSize = PREVIEWS_SIZE.width + 'x' + PREVIEWS_SIZE.height
381
382 return generateImageFromVideoFile(
383 this.getVideoFilePath(videoFile),
384 CONFIG.STORAGE.PREVIEWS_DIR,
385 this.getPreviewName(),
386 imageSize
387 )
388 }
389
390 createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) {
391 const imageSize = THUMBNAILS_SIZE.width + 'x' + THUMBNAILS_SIZE.height
392
393 return generateImageFromVideoFile(
394 this.getVideoFilePath(videoFile),
395 CONFIG.STORAGE.THUMBNAILS_DIR,
396 this.getThumbnailName(),
397 imageSize
398 )
399 }
400
401 getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance) {
402 return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
403 }
404
405 createTorrentAndSetInfoHash = function (this: VideoInstance, videoFile: VideoFileInstance) {
406 const options = {
407 announceList: [
408 [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
409 ],
410 urlList: [
411 CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
412 ]
413 }
414
415 return createTorrentPromise(this.getVideoFilePath(videoFile), options)
416 .then(torrent => {
417 const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
418 logger.info('Creating torrent %s.', filePath)
419
420 return writeFilePromise(filePath, torrent).then(() => torrent)
421 })
422 .then(torrent => {
423 const parsedTorrent = parseTorrent(torrent)
424
425 videoFile.infoHash = parsedTorrent.infoHash
426 })
427 }
428
429 getEmbedPath = function (this: VideoInstance) {
430 return '/videos/embed/' + this.uuid
431 }
432
433 getThumbnailPath = function (this: VideoInstance) {
434 return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName())
435 }
436
437 getPreviewPath = function (this: VideoInstance) {
438 return join(STATIC_PATHS.PREVIEWS, this.getPreviewName())
439 }
440
441 toFormattedJSON = function (this: VideoInstance) {
442 let podHost
443
444 if (this.VideoChannel.Author.Pod) {
445 podHost = this.VideoChannel.Author.Pod.host
446 } else {
447 // It means it's our video
448 podHost = CONFIG.WEBSERVER.HOST
449 }
450
451 // Maybe our pod is not up to date and there are new categories since our version
452 let categoryLabel = VIDEO_CATEGORIES[this.category]
453 if (!categoryLabel) categoryLabel = 'Misc'
454
455 // Maybe our pod is not up to date and there are new licences since our version
456 let licenceLabel = VIDEO_LICENCES[this.licence]
457 if (!licenceLabel) licenceLabel = 'Unknown'
458
459 // Language is an optional attribute
460 let languageLabel = VIDEO_LANGUAGES[this.language]
461 if (!languageLabel) languageLabel = 'Unknown'
462
463 const json = {
464 id: this.id,
465 uuid: this.uuid,
466 name: this.name,
467 category: this.category,
468 categoryLabel,
469 licence: this.licence,
470 licenceLabel,
471 language: this.language,
472 languageLabel,
473 nsfw: this.nsfw,
474 description: this.description,
475 podHost,
476 isLocal: this.isOwned(),
477 author: this.VideoChannel.Author.name,
478 duration: this.duration,
479 views: this.views,
480 likes: this.likes,
481 dislikes: this.dislikes,
482 tags: map<TagInstance, string>(this.Tags, 'name'),
483 thumbnailPath: this.getThumbnailPath(),
484 previewPath: this.getPreviewPath(),
485 embedPath: this.getEmbedPath(),
486 createdAt: this.createdAt,
487 updatedAt: this.updatedAt
488 }
489
490 return json
491 }
492
493 toFormattedDetailsJSON = function (this: VideoInstance) {
494 let podHost
495
496 if (this.VideoChannel.Author.Pod) {
497 podHost = this.VideoChannel.Author.Pod.host
498 } else {
499 // It means it's our video
500 podHost = CONFIG.WEBSERVER.HOST
501 }
502
503 // Maybe our pod is not up to date and there are new categories since our version
504 let categoryLabel = VIDEO_CATEGORIES[this.category]
505 if (!categoryLabel) categoryLabel = 'Misc'
506
507 // Maybe our pod is not up to date and there are new licences since our version
508 let licenceLabel = VIDEO_LICENCES[this.licence]
509 if (!licenceLabel) licenceLabel = 'Unknown'
510
511 // Language is an optional attribute
512 let languageLabel = VIDEO_LANGUAGES[this.language]
513 if (!languageLabel) languageLabel = 'Unknown'
514
515 const json = {
516 id: this.id,
517 uuid: this.uuid,
518 name: this.name,
519 category: this.category,
520 categoryLabel,
521 licence: this.licence,
522 licenceLabel,
523 language: this.language,
524 languageLabel,
525 nsfw: this.nsfw,
526 description: this.description,
527 podHost,
528 isLocal: this.isOwned(),
529 author: this.VideoChannel.Author.name,
530 duration: this.duration,
531 views: this.views,
532 likes: this.likes,
533 dislikes: this.dislikes,
534 tags: map<TagInstance, string>(this.Tags, 'name'),
535 thumbnailPath: this.getThumbnailPath(),
536 previewPath: this.getPreviewPath(),
537 embedPath: this.getEmbedPath(),
538 createdAt: this.createdAt,
539 updatedAt: this.updatedAt,
540 channel: this.VideoChannel.toFormattedJSON(),
541 files: []
542 }
543
544 // Format and sort video files
545 const { baseUrlHttp, baseUrlWs } = getBaseUrls(this)
546 json.files = this.VideoFiles
547 .map(videoFile => {
548 let resolutionLabel = videoFile.resolution + 'p'
549
550 const videoFileJson = {
551 resolution: videoFile.resolution,
552 resolutionLabel,
553 magnetUri: generateMagnetUri(this, videoFile, baseUrlHttp, baseUrlWs),
554 size: videoFile.size,
555 torrentUrl: getTorrentUrl(this, videoFile, baseUrlHttp),
556 fileUrl: getVideoFileUrl(this, videoFile, baseUrlHttp)
557 }
558
559 return videoFileJson
560 })
561 .sort((a, b) => {
562 if (a.resolution < b.resolution) return 1
563 if (a.resolution === b.resolution) return 0
564 return -1
565 })
566
567 return json
568 }
569
570 toAddRemoteJSON = function (this: VideoInstance) {
571 // Get thumbnail data to send to the other pod
572 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
573
574 return readFileBufferPromise(thumbnailPath).then(thumbnailData => {
575 const remoteVideo = {
576 uuid: this.uuid,
577 name: this.name,
578 category: this.category,
579 licence: this.licence,
580 language: this.language,
581 nsfw: this.nsfw,
582 description: this.description,
583 channelUUID: this.VideoChannel.uuid,
584 duration: this.duration,
585 thumbnailData: thumbnailData.toString('binary'),
586 tags: map<TagInstance, string>(this.Tags, 'name'),
587 createdAt: this.createdAt,
588 updatedAt: this.updatedAt,
589 views: this.views,
590 likes: this.likes,
591 dislikes: this.dislikes,
592 files: []
593 }
594
595 this.VideoFiles.forEach(videoFile => {
596 remoteVideo.files.push({
597 infoHash: videoFile.infoHash,
598 resolution: videoFile.resolution,
599 extname: videoFile.extname,
600 size: videoFile.size
601 })
602 })
603
604 return remoteVideo
605 })
606 }
607
608 toUpdateRemoteJSON = function (this: VideoInstance) {
609 const json = {
610 uuid: this.uuid,
611 name: this.name,
612 category: this.category,
613 licence: this.licence,
614 language: this.language,
615 nsfw: this.nsfw,
616 description: this.description,
617 duration: this.duration,
618 tags: map<TagInstance, string>(this.Tags, 'name'),
619 createdAt: this.createdAt,
620 updatedAt: this.updatedAt,
621 views: this.views,
622 likes: this.likes,
623 dislikes: this.dislikes,
624 files: []
625 }
626
627 this.VideoFiles.forEach(videoFile => {
628 json.files.push({
629 infoHash: videoFile.infoHash,
630 resolution: videoFile.resolution,
631 extname: videoFile.extname,
632 size: videoFile.size
633 })
634 })
635
636 return json
637 }
638
639 optimizeOriginalVideofile = function (this: VideoInstance) {
640 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
641 const newExtname = '.mp4'
642 const inputVideoFile = this.getOriginalFile()
643 const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
644 const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
645
646 const transcodeOptions = {
647 inputPath: videoInputPath,
648 outputPath: videoOutputPath
649 }
650
651 return transcode(transcodeOptions)
652 .then(() => {
653 return unlinkPromise(videoInputPath)
654 })
655 .then(() => {
656 // Important to do this before getVideoFilename() to take in account the new file extension
657 inputVideoFile.set('extname', newExtname)
658
659 return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile))
660 })
661 .then(() => {
662 return statPromise(this.getVideoFilePath(inputVideoFile))
663 })
664 .then(stats => {
665 return inputVideoFile.set('size', stats.size)
666 })
667 .then(() => {
668 return this.createTorrentAndSetInfoHash(inputVideoFile)
669 })
670 .then(() => {
671 return inputVideoFile.save()
672 })
673 .then(() => {
674 return undefined
675 })
676 .catch(err => {
677 // Auto destruction...
678 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
679
680 throw err
681 })
682 }
683
684 transcodeOriginalVideofile = function (this: VideoInstance, resolution: VideoResolution) {
685 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
686 const extname = '.mp4'
687
688 // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
689 const videoInputPath = join(videosDirectory, this.getVideoFilename(this.getOriginalFile()))
690
691 const newVideoFile = (Video['sequelize'].models.VideoFile as VideoFileModel).build({
692 resolution,
693 extname,
694 size: 0,
695 videoId: this.id
696 })
697 const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile))
698
699 const transcodeOptions = {
700 inputPath: videoInputPath,
701 outputPath: videoOutputPath,
702 resolution
703 }
704 return transcode(transcodeOptions)
705 .then(() => {
706 return statPromise(videoOutputPath)
707 })
708 .then(stats => {
709 newVideoFile.set('size', stats.size)
710
711 return undefined
712 })
713 .then(() => {
714 return this.createTorrentAndSetInfoHash(newVideoFile)
715 })
716 .then(() => {
717 return newVideoFile.save()
718 })
719 .then(() => {
720 return this.VideoFiles.push(newVideoFile)
721 })
722 .then(() => undefined)
723 }
724
725 getOriginalFileHeight = function (this: VideoInstance) {
726 const originalFilePath = this.getVideoFilePath(this.getOriginalFile())
727
728 return getVideoFileHeight(originalFilePath)
729 }
730
731 removeThumbnail = function (this: VideoInstance) {
732 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
733 return unlinkPromise(thumbnailPath)
734 }
735
736 removePreview = function (this: VideoInstance) {
737 // Same name than video thumbnail
738 return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName())
739 }
740
741 removeFile = function (this: VideoInstance, videoFile: VideoFileInstance) {
742 const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
743 return unlinkPromise(filePath)
744 }
745
746 removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) {
747 const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
748 return unlinkPromise(torrentPath)
749 }
750
751 // ------------------------------ STATICS ------------------------------
752
753 generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) {
754 // Creating the thumbnail for a remote video
755
756 const thumbnailName = video.getThumbnailName()
757 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
758 return writeFilePromise(thumbnailPath, Buffer.from(thumbnailData, 'binary')).then(() => {
759 return thumbnailName
760 })
761 }
762
763 list = function () {
764 const query = {
765 include: [ Video['sequelize'].models.VideoFile ]
766 }
767
768 return Video.findAll(query)
769 }
770
771 listForApi = function (start: number, count: number, sort: string) {
772 // Exclude blacklisted videos from the list
773 const query = {
774 distinct: true,
775 offset: start,
776 limit: count,
777 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
778 include: [
779 {
780 model: Video['sequelize'].models.VideoChannel,
781 include: [
782 {
783 model: Video['sequelize'].models.Author,
784 include: [
785 {
786 model: Video['sequelize'].models.Pod,
787 required: false
788 }
789 ]
790 }
791 ]
792 },
793 Video['sequelize'].models.Tag,
794 Video['sequelize'].models.VideoFile
795 ],
796 where: createBaseVideosWhere()
797 }
798
799 return Video.findAndCountAll(query).then(({ rows, count }) => {
800 return {
801 data: rows,
802 total: count
803 }
804 })
805 }
806
807 loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Transaction) {
808 const query: Sequelize.FindOptions<VideoAttributes> = {
809 where: {
810 uuid
811 },
812 include: [
813 {
814 model: Video['sequelize'].models.VideoFile
815 },
816 {
817 model: Video['sequelize'].models.VideoChannel,
818 include: [
819 {
820 model: Video['sequelize'].models.Author,
821 include: [
822 {
823 model: Video['sequelize'].models.Pod,
824 required: true,
825 where: {
826 host: fromHost
827 }
828 }
829 ]
830 }
831 ]
832 }
833 ]
834 }
835
836 if (t !== undefined) query.transaction = t
837
838 return Video.findOne(query)
839 }
840
841 listOwnedAndPopulateAuthorAndTags = function () {
842 const query = {
843 where: {
844 remote: false
845 },
846 include: [
847 Video['sequelize'].models.VideoFile,
848 {
849 model: Video['sequelize'].models.VideoChannel,
850 include: [ Video['sequelize'].models.Author ]
851 },
852 Video['sequelize'].models.Tag
853 ]
854 }
855
856 return Video.findAll(query)
857 }
858
859 listOwnedByAuthor = function (author: string) {
860 const query = {
861 where: {
862 remote: false
863 },
864 include: [
865 {
866 model: Video['sequelize'].models.VideoFile
867 },
868 {
869 model: Video['sequelize'].models.VideoChannel,
870 include: [
871 {
872 model: Video['sequelize'].models.Author,
873 where: {
874 name: author
875 }
876 }
877 ]
878 }
879 ]
880 }
881
882 return Video.findAll(query)
883 }
884
885 load = function (id: number) {
886 return Video.findById(id)
887 }
888
889 loadByUUID = function (uuid: string, t?: Sequelize.Transaction) {
890 const query: Sequelize.FindOptions<VideoAttributes> = {
891 where: {
892 uuid
893 },
894 include: [ Video['sequelize'].models.VideoFile ]
895 }
896
897 if (t !== undefined) query.transaction = t
898
899 return Video.findOne(query)
900 }
901
902 loadAndPopulateAuthor = function (id: number) {
903 const options = {
904 include: [
905 Video['sequelize'].models.VideoFile,
906 {
907 model: Video['sequelize'].models.VideoChannel,
908 include: [ Video['sequelize'].models.Author ]
909 }
910 ]
911 }
912
913 return Video.findById(id, options)
914 }
915
916 loadAndPopulateAuthorAndPodAndTags = function (id: number) {
917 const options = {
918 include: [
919 {
920 model: Video['sequelize'].models.VideoChannel,
921 include: [
922 {
923 model: Video['sequelize'].models.Author,
924 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
925 }
926 ]
927 },
928 Video['sequelize'].models.Tag,
929 Video['sequelize'].models.VideoFile
930 ]
931 }
932
933 return Video.findById(id, options)
934 }
935
936 loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) {
937 const options = {
938 where: {
939 uuid
940 },
941 include: [
942 {
943 model: Video['sequelize'].models.VideoChannel,
944 include: [
945 {
946 model: Video['sequelize'].models.Author,
947 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
948 }
949 ]
950 },
951 Video['sequelize'].models.Tag,
952 Video['sequelize'].models.VideoFile
953 ]
954 }
955
956 return Video.findOne(options)
957 }
958
959 searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, start: number, count: number, sort: string) {
960 const podInclude: Sequelize.IncludeOptions = {
961 model: Video['sequelize'].models.Pod,
962 required: false
963 }
964
965 const authorInclude: Sequelize.IncludeOptions = {
966 model: Video['sequelize'].models.Author,
967 include: [ podInclude ]
968 }
969
970 const videoChannelInclude: Sequelize.IncludeOptions = {
971 model: Video['sequelize'].models.VideoChannel,
972 include: [ authorInclude ],
973 required: true
974 }
975
976 const tagInclude: Sequelize.IncludeOptions = {
977 model: Video['sequelize'].models.Tag
978 }
979
980 const videoFileInclude: Sequelize.IncludeOptions = {
981 model: Video['sequelize'].models.VideoFile
982 }
983
984 const query: Sequelize.FindOptions<VideoAttributes> = {
985 distinct: true,
986 where: createBaseVideosWhere(),
987 offset: start,
988 limit: count,
989 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ]
990 }
991
992 // Make an exact search with the magnet
993 if (field === 'magnetUri') {
994 videoFileInclude.where = {
995 infoHash: magnetUtil.decode(value).infoHash
996 }
997 } else if (field === 'tags') {
998 const escapedValue = Video['sequelize'].escape('%' + value + '%')
999 query.where['id'].$in = Video['sequelize'].literal(
1000 `(SELECT "VideoTags"."videoId"
1001 FROM "Tags"
1002 INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId"
1003 WHERE name ILIKE ${escapedValue}
1004 )`
1005 )
1006 } else if (field === 'host') {
1007 // FIXME: Include our pod? (not stored in the database)
1008 podInclude.where = {
1009 host: {
1010 $iLike: '%' + value + '%'
1011 }
1012 }
1013 podInclude.required = true
1014 } else if (field === 'author') {
1015 authorInclude.where = {
1016 name: {
1017 $iLike: '%' + value + '%'
1018 }
1019 }
1020 } else {
1021 query.where[field] = {
1022 $iLike: '%' + value + '%'
1023 }
1024 }
1025
1026 query.include = [
1027 videoChannelInclude, tagInclude, videoFileInclude
1028 ]
1029
1030 return Video.findAndCountAll(query).then(({ rows, count }) => {
1031 return {
1032 data: rows,
1033 total: count
1034 }
1035 })
1036 }
1037
1038 // ---------------------------------------------------------------------------
1039
1040 function createBaseVideosWhere () {
1041 return {
1042 id: {
1043 $notIn: Video['sequelize'].literal(
1044 '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")'
1045 )
1046 }
1047 }
1048 }
1049
1050 function getBaseUrls (video: VideoInstance) {
1051 let baseUrlHttp
1052 let baseUrlWs
1053
1054 if (video.isOwned()) {
1055 baseUrlHttp = CONFIG.WEBSERVER.URL
1056 baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
1057 } else {
1058 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + video.VideoChannel.Author.Pod.host
1059 baseUrlWs = REMOTE_SCHEME.WS + '://' + video.VideoChannel.Author.Pod.host
1060 }
1061
1062 return { baseUrlHttp, baseUrlWs }
1063 }
1064
1065 function getTorrentUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) {
1066 return baseUrlHttp + STATIC_PATHS.TORRENTS + video.getTorrentFileName(videoFile)
1067 }
1068
1069 function getVideoFileUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) {
1070 return baseUrlHttp + STATIC_PATHS.WEBSEED + video.getVideoFilename(videoFile)
1071 }
1072
1073 function generateMagnetUri (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string, baseUrlWs: string) {
1074 const xs = getTorrentUrl(video, videoFile, baseUrlHttp)
1075 const announce = [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
1076 const urlList = [ getVideoFileUrl(video, videoFile, baseUrlHttp) ]
1077
1078 const magnetHash = {
1079 xs,
1080 announce,
1081 urlList,
1082 infoHash: videoFile.infoHash,
1083 name: video.name
1084 }
1085
1086 return magnetUtil.encode(magnetHash)
1087 }