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