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