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 import * as Promise from 'bluebird'
10 import { TagInstance } from './tag-interface'
18 isVideoDescriptionValid,
21 readFileBufferPromise,
27 generateImageFromVideoFile,
30 } from '../../helpers'
43 } from '../../initializers'
44 import { removeVideoToFriends } from '../../lib'
45 import { VideoResolution, VideoPrivacy } from '../../../shared'
46 import { VideoFileInstance, VideoFileModel } from './video-file-interface'
48 import { addMethodsToModel, getSort } from '../utils'
54 } from './video-interface'
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 toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
68 let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
69 let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
70 let transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
71 let createPreview: VideoMethods.CreatePreview
72 let createThumbnail: VideoMethods.CreateThumbnail
73 let getVideoFilePath: VideoMethods.GetVideoFilePath
74 let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
75 let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
76 let getEmbedPath: VideoMethods.GetEmbedPath
77 let getDescriptionPath: VideoMethods.GetDescriptionPath
78 let getTruncatedDescription: VideoMethods.GetTruncatedDescription
80 let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
81 let list: VideoMethods.List
82 let listForApi: VideoMethods.ListForApi
83 let listUserVideosForApi: VideoMethods.ListUserVideosForApi
84 let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
85 let listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags
86 let listOwnedByAuthor: VideoMethods.ListOwnedByAuthor
87 let load: VideoMethods.Load
88 let loadByUUID: VideoMethods.LoadByUUID
89 let loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID
90 let loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor
91 let loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags
92 let loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags
93 let searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags
94 let removeThumbnail: VideoMethods.RemoveThumbnail
95 let removePreview: VideoMethods.RemovePreview
96 let removeFile: VideoMethods.RemoveFile
97 let removeTorrent: VideoMethods.RemoveTorrent
99 export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
100 Video = sequelize.define<VideoInstance, VideoAttributes>('Video',
103 type: DataTypes.UUID,
104 defaultValue: DataTypes.UUIDV4,
111 type: DataTypes.STRING,
114 nameValid: value => {
115 const res = isVideoNameValid(value)
116 if (res === false) throw new Error('Video name is not valid.')
121 type: DataTypes.INTEGER,
124 categoryValid: value => {
125 const res = isVideoCategoryValid(value)
126 if (res === false) throw new Error('Video category is not valid.')
131 type: DataTypes.INTEGER,
135 licenceValid: value => {
136 const res = isVideoLicenceValid(value)
137 if (res === false) throw new Error('Video licence is not valid.')
142 type: DataTypes.INTEGER,
145 languageValid: value => {
146 const res = isVideoLanguageValid(value)
147 if (res === false) throw new Error('Video language is not valid.')
152 type: DataTypes.INTEGER,
155 privacyValid: value => {
156 const res = isVideoPrivacyValid(value)
157 if (res === false) throw new Error('Video privacy is not valid.')
162 type: DataTypes.BOOLEAN,
165 nsfwValid: value => {
166 const res = isVideoNSFWValid(value)
167 if (res === false) throw new Error('Video nsfw attribute is not valid.')
172 type: DataTypes.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max),
175 descriptionValid: value => {
176 const res = isVideoDescriptionValid(value)
177 if (res === false) throw new Error('Video description is not valid.')
182 type: DataTypes.INTEGER,
185 durationValid: value => {
186 const res = isVideoDurationValid(value)
187 if (res === false) throw new Error('Video duration is not valid.')
192 type: DataTypes.INTEGER,
201 type: DataTypes.INTEGER,
210 type: DataTypes.INTEGER,
219 type: DataTypes.BOOLEAN,
230 fields: [ 'createdAt' ]
233 fields: [ 'duration' ]
245 fields: [ 'channelId' ]
254 const classMethods = [
257 generateThumbnailFromData,
260 listUserVideosForApi,
261 listOwnedAndPopulateAuthorAndTags,
264 loadAndPopulateAuthor,
265 loadAndPopulateAuthorAndPodAndTags,
268 loadLocalVideoByUUID,
269 loadByUUIDAndPopulateAuthorAndPodAndTags,
270 searchAndPopulateAuthorAndPodAndTags
272 const instanceMethods = [
275 createTorrentAndSetInfoHash,
291 toFormattedDetailsJSON,
293 optimizeOriginalVideofile,
294 transcodeOriginalVideofile,
295 getOriginalFileHeight,
297 getTruncatedDescription,
300 addMethodsToModel(Video, classMethods, instanceMethods)
305 // ------------------------------ METHODS ------------------------------
307 function associate (models) {
308 Video.belongsTo(models.VideoChannel, {
316 Video.belongsToMany(models.Tag, {
317 foreignKey: 'videoId',
318 through: models.VideoTag,
322 Video.hasMany(models.VideoAbuse, {
330 Video.hasMany(models.VideoFile, {
339 function afterDestroy (video: VideoInstance) {
343 video.removeThumbnail()
346 if (video.isOwned()) {
347 const removeVideoToFriendsParams = {
352 video.removePreview(),
353 removeVideoToFriends(removeVideoToFriendsParams)
356 // Remove physical files and torrents
357 video.VideoFiles.forEach(file => {
358 tasks.push(video.removeFile(file))
359 tasks.push(video.removeTorrent(file))
363 return Promise.all(tasks)
365 logger.error('Some errors when removing files of video %s in after destroy hook.', video.uuid, err)
369 getOriginalFile = function (this: VideoInstance) {
370 if (Array.isArray(this.VideoFiles) === false) return undefined
372 // The original file is the file that have the higher resolution
373 return maxBy(this.VideoFiles, file => file.resolution)
376 getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) {
377 return this.uuid + '-' + videoFile.resolution + videoFile.extname
380 getThumbnailName = function (this: VideoInstance) {
381 // We always have a copy of the thumbnail
382 const extension = '.jpg'
383 return this.uuid + extension
386 getPreviewName = function (this: VideoInstance) {
387 const extension = '.jpg'
388 return this.uuid + extension
391 getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) {
392 const extension = '.torrent'
393 return this.uuid + '-' + videoFile.resolution + extension
396 isOwned = function (this: VideoInstance) {
397 return this.remote === false
400 createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) {
401 const imageSize = PREVIEWS_SIZE.width + 'x' + PREVIEWS_SIZE.height
403 return generateImageFromVideoFile(
404 this.getVideoFilePath(videoFile),
405 CONFIG.STORAGE.PREVIEWS_DIR,
406 this.getPreviewName(),
411 createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) {
412 const imageSize = THUMBNAILS_SIZE.width + 'x' + THUMBNAILS_SIZE.height
414 return generateImageFromVideoFile(
415 this.getVideoFilePath(videoFile),
416 CONFIG.STORAGE.THUMBNAILS_DIR,
417 this.getThumbnailName(),
422 getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance) {
423 return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
426 createTorrentAndSetInfoHash = function (this: VideoInstance, videoFile: VideoFileInstance) {
429 [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
432 CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
436 return createTorrentPromise(this.getVideoFilePath(videoFile), options)
438 const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
439 logger.info('Creating torrent %s.', filePath)
441 return writeFilePromise(filePath, torrent).then(() => torrent)
444 const parsedTorrent = parseTorrent(torrent)
446 videoFile.infoHash = parsedTorrent.infoHash
450 getEmbedPath = function (this: VideoInstance) {
451 return '/videos/embed/' + this.uuid
454 getThumbnailPath = function (this: VideoInstance) {
455 return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName())
458 getPreviewPath = function (this: VideoInstance) {
459 return join(STATIC_PATHS.PREVIEWS, this.getPreviewName())
462 toFormattedJSON = function (this: VideoInstance) {
465 if (this.VideoChannel.Author.Pod) {
466 podHost = this.VideoChannel.Author.Pod.host
468 // It means it's our video
469 podHost = CONFIG.WEBSERVER.HOST
472 // Maybe our pod is not up to date and there are new categories since our version
473 let categoryLabel = VIDEO_CATEGORIES[this.category]
474 if (!categoryLabel) categoryLabel = 'Misc'
476 // Maybe our pod is not up to date and there are new licences since our version
477 let licenceLabel = VIDEO_LICENCES[this.licence]
478 if (!licenceLabel) licenceLabel = 'Unknown'
480 // Language is an optional attribute
481 let languageLabel = VIDEO_LANGUAGES[this.language]
482 if (!languageLabel) languageLabel = 'Unknown'
488 category: this.category,
490 licence: this.licence,
492 language: this.language,
495 description: this.getTruncatedDescription(),
497 isLocal: this.isOwned(),
498 author: this.VideoChannel.Author.name,
499 duration: this.duration,
502 dislikes: this.dislikes,
503 tags: map<TagInstance, string>(this.Tags, 'name'),
504 thumbnailPath: this.getThumbnailPath(),
505 previewPath: this.getPreviewPath(),
506 embedPath: this.getEmbedPath(),
507 createdAt: this.createdAt,
508 updatedAt: this.updatedAt
514 toFormattedDetailsJSON = function (this: VideoInstance) {
515 const formattedJson = this.toFormattedJSON()
517 // Maybe our pod is not up to date and there are new privacy settings since our version
518 let privacyLabel = VIDEO_PRIVACIES[this.privacy]
519 if (!privacyLabel) privacyLabel = 'Unknown'
521 const detailsJson = {
523 privacy: this.privacy,
524 descriptionPath: this.getDescriptionPath(),
525 channel: this.VideoChannel.toFormattedJSON(),
529 // Format and sort video files
530 const { baseUrlHttp, baseUrlWs } = getBaseUrls(this)
531 detailsJson.files = this.VideoFiles
533 let resolutionLabel = videoFile.resolution + 'p'
535 const videoFileJson = {
536 resolution: videoFile.resolution,
538 magnetUri: generateMagnetUri(this, videoFile, baseUrlHttp, baseUrlWs),
539 size: videoFile.size,
540 torrentUrl: getTorrentUrl(this, videoFile, baseUrlHttp),
541 fileUrl: getVideoFileUrl(this, videoFile, baseUrlHttp)
547 if (a.resolution < b.resolution) return 1
548 if (a.resolution === b.resolution) return 0
552 return Object.assign(formattedJson, detailsJson)
555 toAddRemoteJSON = function (this: VideoInstance) {
556 // Get thumbnail data to send to the other pod
557 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
559 return readFileBufferPromise(thumbnailPath).then(thumbnailData => {
560 const remoteVideo = {
563 category: this.category,
564 licence: this.licence,
565 language: this.language,
567 truncatedDescription: this.getTruncatedDescription(),
568 channelUUID: this.VideoChannel.uuid,
569 duration: this.duration,
570 thumbnailData: thumbnailData.toString('binary'),
571 tags: map<TagInstance, string>(this.Tags, 'name'),
572 createdAt: this.createdAt,
573 updatedAt: this.updatedAt,
576 dislikes: this.dislikes,
577 privacy: this.privacy,
581 this.VideoFiles.forEach(videoFile => {
582 remoteVideo.files.push({
583 infoHash: videoFile.infoHash,
584 resolution: videoFile.resolution,
585 extname: videoFile.extname,
594 toUpdateRemoteJSON = function (this: VideoInstance) {
598 category: this.category,
599 licence: this.licence,
600 language: this.language,
602 truncatedDescription: this.getTruncatedDescription(),
603 duration: this.duration,
604 tags: map<TagInstance, string>(this.Tags, 'name'),
605 createdAt: this.createdAt,
606 updatedAt: this.updatedAt,
609 dislikes: this.dislikes,
610 privacy: this.privacy,
614 this.VideoFiles.forEach(videoFile => {
616 infoHash: videoFile.infoHash,
617 resolution: videoFile.resolution,
618 extname: videoFile.extname,
626 getTruncatedDescription = function (this: VideoInstance) {
628 length: CONSTRAINTS_FIELDS.VIDEOS.TRUNCATED_DESCRIPTION.max
631 return truncate(this.description, options)
634 optimizeOriginalVideofile = function (this: VideoInstance) {
635 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
636 const newExtname = '.mp4'
637 const inputVideoFile = this.getOriginalFile()
638 const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
639 const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
641 const transcodeOptions = {
642 inputPath: videoInputPath,
643 outputPath: videoOutputPath
646 return transcode(transcodeOptions)
648 return unlinkPromise(videoInputPath)
651 // Important to do this before getVideoFilename() to take in account the new file extension
652 inputVideoFile.set('extname', newExtname)
654 return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile))
657 return statPromise(this.getVideoFilePath(inputVideoFile))
660 return inputVideoFile.set('size', stats.size)
663 return this.createTorrentAndSetInfoHash(inputVideoFile)
666 return inputVideoFile.save()
672 // Auto destruction...
673 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
679 transcodeOriginalVideofile = function (this: VideoInstance, resolution: VideoResolution) {
680 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
681 const extname = '.mp4'
683 // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
684 const videoInputPath = join(videosDirectory, this.getVideoFilename(this.getOriginalFile()))
686 const newVideoFile = (Video['sequelize'].models.VideoFile as VideoFileModel).build({
692 const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile))
694 const transcodeOptions = {
695 inputPath: videoInputPath,
696 outputPath: videoOutputPath,
699 return transcode(transcodeOptions)
701 return statPromise(videoOutputPath)
704 newVideoFile.set('size', stats.size)
709 return this.createTorrentAndSetInfoHash(newVideoFile)
712 return newVideoFile.save()
715 return this.VideoFiles.push(newVideoFile)
717 .then(() => undefined)
720 getOriginalFileHeight = function (this: VideoInstance) {
721 const originalFilePath = this.getVideoFilePath(this.getOriginalFile())
723 return getVideoFileHeight(originalFilePath)
726 getDescriptionPath = function (this: VideoInstance) {
727 return `/api/${API_VERSION}/videos/${this.uuid}/description`
730 removeThumbnail = function (this: VideoInstance) {
731 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
732 return unlinkPromise(thumbnailPath)
735 removePreview = function (this: VideoInstance) {
736 // Same name than video thumbnail
737 return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName())
740 removeFile = function (this: VideoInstance, videoFile: VideoFileInstance) {
741 const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
742 return unlinkPromise(filePath)
745 removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) {
746 const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
747 return unlinkPromise(torrentPath)
750 // ------------------------------ STATICS ------------------------------
752 generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) {
753 // Creating the thumbnail for a remote video
755 const thumbnailName = video.getThumbnailName()
756 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
757 return writeFilePromise(thumbnailPath, Buffer.from(thumbnailData, 'binary')).then(() => {
764 include: [ Video['sequelize'].models.VideoFile ]
767 return Video.findAll(query)
770 listUserVideosForApi = function (userId: number, start: number, count: number, sort: string) {
775 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
778 model: Video['sequelize'].models.VideoChannel,
782 model: Video['sequelize'].models.Author,
790 Video['sequelize'].models.Tag
794 return Video.findAndCountAll(query).then(({ rows, count }) => {
802 listForApi = function (start: number, count: number, sort: string) {
807 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
810 model: Video['sequelize'].models.VideoChannel,
813 model: Video['sequelize'].models.Author,
816 model: Video['sequelize'].models.Pod,
823 Video['sequelize'].models.Tag
825 where: createBaseVideosWhere()
828 return Video.findAndCountAll(query).then(({ rows, count }) => {
836 loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Transaction) {
837 const query: Sequelize.FindOptions<VideoAttributes> = {
843 model: Video['sequelize'].models.VideoFile
846 model: Video['sequelize'].models.VideoChannel,
849 model: Video['sequelize'].models.Author,
852 model: Video['sequelize'].models.Pod,
865 if (t !== undefined) query.transaction = t
867 return Video.findOne(query)
870 listOwnedAndPopulateAuthorAndTags = function () {
876 Video['sequelize'].models.VideoFile,
878 model: Video['sequelize'].models.VideoChannel,
879 include: [ Video['sequelize'].models.Author ]
881 Video['sequelize'].models.Tag
885 return Video.findAll(query)
888 listOwnedByAuthor = function (author: string) {
895 model: Video['sequelize'].models.VideoFile
898 model: Video['sequelize'].models.VideoChannel,
901 model: Video['sequelize'].models.Author,
911 return Video.findAll(query)
914 load = function (id: number) {
915 return Video.findById(id)
918 loadByUUID = function (uuid: string, t?: Sequelize.Transaction) {
919 const query: Sequelize.FindOptions<VideoAttributes> = {
923 include: [ Video['sequelize'].models.VideoFile ]
926 if (t !== undefined) query.transaction = t
928 return Video.findOne(query)
931 loadLocalVideoByUUID = function (uuid: string, t?: Sequelize.Transaction) {
932 const query: Sequelize.FindOptions<VideoAttributes> = {
937 include: [ Video['sequelize'].models.VideoFile ]
940 if (t !== undefined) query.transaction = t
942 return Video.findOne(query)
945 loadAndPopulateAuthor = function (id: number) {
948 Video['sequelize'].models.VideoFile,
950 model: Video['sequelize'].models.VideoChannel,
951 include: [ Video['sequelize'].models.Author ]
956 return Video.findById(id, options)
959 loadAndPopulateAuthorAndPodAndTags = function (id: number) {
963 model: Video['sequelize'].models.VideoChannel,
966 model: Video['sequelize'].models.Author,
967 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
971 Video['sequelize'].models.Tag,
972 Video['sequelize'].models.VideoFile
976 return Video.findById(id, options)
979 loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) {
986 model: Video['sequelize'].models.VideoChannel,
989 model: Video['sequelize'].models.Author,
990 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
994 Video['sequelize'].models.Tag,
995 Video['sequelize'].models.VideoFile
999 return Video.findOne(options)
1002 searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, start: number, count: number, sort: string) {
1003 const podInclude: Sequelize.IncludeOptions = {
1004 model: Video['sequelize'].models.Pod,
1008 const authorInclude: Sequelize.IncludeOptions = {
1009 model: Video['sequelize'].models.Author,
1010 include: [ podInclude ]
1013 const videoChannelInclude: Sequelize.IncludeOptions = {
1014 model: Video['sequelize'].models.VideoChannel,
1015 include: [ authorInclude ],
1019 const tagInclude: Sequelize.IncludeOptions = {
1020 model: Video['sequelize'].models.Tag
1023 const query: Sequelize.FindOptions<VideoAttributes> = {
1025 where: createBaseVideosWhere(),
1028 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ]
1031 if (field === 'tags') {
1032 const escapedValue = Video['sequelize'].escape('%' + value + '%')
1033 query.where['id'][Sequelize.Op.in] = Video['sequelize'].literal(
1034 `(SELECT "VideoTags"."videoId"
1036 INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId"
1037 WHERE name ILIKE ${escapedValue}
1040 } else if (field === 'host') {
1041 // FIXME: Include our pod? (not stored in the database)
1042 podInclude.where = {
1044 [Sequelize.Op.iLike]: '%' + value + '%'
1047 podInclude.required = true
1048 } else if (field === 'author') {
1049 authorInclude.where = {
1051 [Sequelize.Op.iLike]: '%' + value + '%'
1055 query.where[field] = {
1056 [Sequelize.Op.iLike]: '%' + value + '%'
1061 videoChannelInclude, tagInclude
1064 return Video.findAndCountAll(query).then(({ rows, count }) => {
1072 // ---------------------------------------------------------------------------
1074 function createBaseVideosWhere () {
1077 [Sequelize.Op.notIn]: Video['sequelize'].literal(
1078 '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")'
1081 privacy: VideoPrivacy.PUBLIC
1085 function getBaseUrls (video: VideoInstance) {
1089 if (video.isOwned()) {
1090 baseUrlHttp = CONFIG.WEBSERVER.URL
1091 baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
1093 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + video.VideoChannel.Author.Pod.host
1094 baseUrlWs = REMOTE_SCHEME.WS + '://' + video.VideoChannel.Author.Pod.host
1097 return { baseUrlHttp, baseUrlWs }
1100 function getTorrentUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) {
1101 return baseUrlHttp + STATIC_PATHS.TORRENTS + video.getTorrentFileName(videoFile)
1104 function getVideoFileUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) {
1105 return baseUrlHttp + STATIC_PATHS.WEBSEED + video.getVideoFilename(videoFile)
1108 function generateMagnetUri (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string, baseUrlWs: string) {
1109 const xs = getTorrentUrl(video, videoFile, baseUrlHttp)
1110 const announce = [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
1111 const urlList = [ getVideoFileUrl(video, videoFile, baseUrlHttp) ]
1113 const magnetHash = {
1117 infoHash: videoFile.infoHash,
1121 return magnetUtil.encode(magnetHash)