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