1 import * as safeBuffer from 'safe-buffer'
2 const Buffer = safeBuffer.Buffer
3 import * as magnetUtil from 'magnet-uri'
4 import { map } from 'lodash'
5 import * as parseTorrent from 'parse-torrent'
6 import { join } from 'path'
7 import * as Sequelize from 'sequelize'
8 import * as Promise from 'bluebird'
9 import { maxBy } from 'lodash'
11 import { TagInstance } from './tag-interface'
19 isVideoDescriptionValid,
21 readFileBufferPromise,
27 generateImageFromVideoFile,
30 } from '../../helpers'
39 } from '../../initializers'
40 import { removeVideoToFriends } from '../../lib'
41 import { VideoResolution } from '../../../shared'
42 import { VideoFileInstance, VideoFileModel } from './video-file-interface'
44 import { addMethodsToModel, getSort } from '../utils'
50 } from './video-interface'
51 import { PREVIEWS_SIZE } from '../../initializers/constants'
53 let Video: Sequelize.Model<VideoInstance, VideoAttributes>
54 let getOriginalFile: VideoMethods.GetOriginalFile
55 let getVideoFilename: VideoMethods.GetVideoFilename
56 let getThumbnailName: VideoMethods.GetThumbnailName
57 let getThumbnailPath: VideoMethods.GetThumbnailPath
58 let getPreviewName: VideoMethods.GetPreviewName
59 let getPreviewPath: VideoMethods.GetPreviewPath
60 let getTorrentFileName: VideoMethods.GetTorrentFileName
61 let isOwned: VideoMethods.IsOwned
62 let toFormattedJSON: VideoMethods.ToFormattedJSON
63 let toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON
64 let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
65 let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
66 let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
67 let transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
68 let createPreview: VideoMethods.CreatePreview
69 let createThumbnail: VideoMethods.CreateThumbnail
70 let getVideoFilePath: VideoMethods.GetVideoFilePath
71 let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
72 let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
73 let getEmbedPath: VideoMethods.GetEmbedPath
75 let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
76 let list: VideoMethods.List
77 let listForApi: VideoMethods.ListForApi
78 let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
79 let listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags
80 let listOwnedByAuthor: VideoMethods.ListOwnedByAuthor
81 let load: VideoMethods.Load
82 let loadByUUID: VideoMethods.LoadByUUID
83 let loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID
84 let loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor
85 let loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags
86 let loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags
87 let searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags
88 let removeThumbnail: VideoMethods.RemoveThumbnail
89 let removePreview: VideoMethods.RemovePreview
90 let removeFile: VideoMethods.RemoveFile
91 let removeTorrent: VideoMethods.RemoveTorrent
93 export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
94 Video = sequelize.define<VideoInstance, VideoAttributes>('Video',
98 defaultValue: DataTypes.UUIDV4,
105 type: DataTypes.STRING,
108 nameValid: value => {
109 const res = isVideoNameValid(value)
110 if (res === false) throw new Error('Video name is not valid.')
115 type: DataTypes.INTEGER,
118 categoryValid: value => {
119 const res = isVideoCategoryValid(value)
120 if (res === false) throw new Error('Video category is not valid.')
125 type: DataTypes.INTEGER,
129 licenceValid: value => {
130 const res = isVideoLicenceValid(value)
131 if (res === false) throw new Error('Video licence is not valid.')
136 type: DataTypes.INTEGER,
139 languageValid: value => {
140 const res = isVideoLanguageValid(value)
141 if (res === false) throw new Error('Video language is not valid.')
146 type: DataTypes.BOOLEAN,
149 nsfwValid: value => {
150 const res = isVideoNSFWValid(value)
151 if (res === false) throw new Error('Video nsfw attribute is not valid.')
156 type: DataTypes.STRING,
159 descriptionValid: value => {
160 const res = isVideoDescriptionValid(value)
161 if (res === false) throw new Error('Video description is not valid.')
166 type: DataTypes.INTEGER,
169 durationValid: value => {
170 const res = isVideoDurationValid(value)
171 if (res === false) throw new Error('Video duration is not valid.')
176 type: DataTypes.INTEGER,
185 type: DataTypes.INTEGER,
194 type: DataTypes.INTEGER,
203 type: DataTypes.BOOLEAN,
214 fields: [ 'createdAt' ]
217 fields: [ 'duration' ]
229 fields: [ 'channelId' ]
238 const classMethods = [
241 generateThumbnailFromData,
244 listOwnedAndPopulateAuthorAndTags,
247 loadAndPopulateAuthor,
248 loadAndPopulateAuthorAndPodAndTags,
251 loadLocalVideoByUUID,
252 loadByUUIDAndPopulateAuthorAndPodAndTags,
253 searchAndPopulateAuthorAndPodAndTags
255 const instanceMethods = [
258 createTorrentAndSetInfoHash,
274 toFormattedDetailsJSON,
276 optimizeOriginalVideofile,
277 transcodeOriginalVideofile,
278 getOriginalFileHeight,
281 addMethodsToModel(Video, classMethods, instanceMethods)
286 // ------------------------------ METHODS ------------------------------
288 function associate (models) {
289 Video.belongsTo(models.VideoChannel, {
297 Video.belongsToMany(models.Tag, {
298 foreignKey: 'videoId',
299 through: models.VideoTag,
303 Video.hasMany(models.VideoAbuse, {
311 Video.hasMany(models.VideoFile, {
320 function afterDestroy (video: VideoInstance, options: { transaction: Sequelize.Transaction }) {
324 video.removeThumbnail()
327 if (video.isOwned()) {
328 const removeVideoToFriendsParams = {
333 video.removePreview(),
334 removeVideoToFriends(removeVideoToFriendsParams, options.transaction)
337 // Remove physical files and torrents
338 video.VideoFiles.forEach(file => {
339 tasks.push(video.removeFile(file))
340 tasks.push(video.removeTorrent(file))
344 return Promise.all(tasks)
346 logger.error('Some errors when removing files of video %s in after destroy hook.', video.uuid, err)
350 getOriginalFile = function (this: VideoInstance) {
351 if (Array.isArray(this.VideoFiles) === false) return undefined
353 // The original file is the file that have the higher resolution
354 return maxBy(this.VideoFiles, file => file.resolution)
357 getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) {
358 return this.uuid + '-' + videoFile.resolution + videoFile.extname
361 getThumbnailName = function (this: VideoInstance) {
362 // We always have a copy of the thumbnail
363 const extension = '.jpg'
364 return this.uuid + extension
367 getPreviewName = function (this: VideoInstance) {
368 const extension = '.jpg'
369 return this.uuid + extension
372 getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) {
373 const extension = '.torrent'
374 return this.uuid + '-' + videoFile.resolution + extension
377 isOwned = function (this: VideoInstance) {
378 return this.remote === false
381 createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) {
382 const imageSize = PREVIEWS_SIZE.width + 'x' + PREVIEWS_SIZE.height
384 return generateImageFromVideoFile(
385 this.getVideoFilePath(videoFile),
386 CONFIG.STORAGE.PREVIEWS_DIR,
387 this.getPreviewName(),
392 createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) {
393 const imageSize = THUMBNAILS_SIZE.width + 'x' + THUMBNAILS_SIZE.height
395 return generateImageFromVideoFile(
396 this.getVideoFilePath(videoFile),
397 CONFIG.STORAGE.THUMBNAILS_DIR,
398 this.getThumbnailName(),
403 getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance) {
404 return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
407 createTorrentAndSetInfoHash = function (this: VideoInstance, videoFile: VideoFileInstance) {
410 [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
413 CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
417 return createTorrentPromise(this.getVideoFilePath(videoFile), options)
419 const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
420 logger.info('Creating torrent %s.', filePath)
422 return writeFilePromise(filePath, torrent).then(() => torrent)
425 const parsedTorrent = parseTorrent(torrent)
427 videoFile.infoHash = parsedTorrent.infoHash
431 getEmbedPath = function (this: VideoInstance) {
432 return '/videos/embed/' + this.uuid
435 getThumbnailPath = function (this: VideoInstance) {
436 return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName())
439 getPreviewPath = function (this: VideoInstance) {
440 return join(STATIC_PATHS.PREVIEWS, this.getPreviewName())
443 toFormattedJSON = function (this: VideoInstance) {
446 if (this.VideoChannel.Author.Pod) {
447 podHost = this.VideoChannel.Author.Pod.host
449 // It means it's our video
450 podHost = CONFIG.WEBSERVER.HOST
453 // Maybe our pod is not up to date and there are new categories since our version
454 let categoryLabel = VIDEO_CATEGORIES[this.category]
455 if (!categoryLabel) categoryLabel = 'Misc'
457 // Maybe our pod is not up to date and there are new licences since our version
458 let licenceLabel = VIDEO_LICENCES[this.licence]
459 if (!licenceLabel) licenceLabel = 'Unknown'
461 // Language is an optional attribute
462 let languageLabel = VIDEO_LANGUAGES[this.language]
463 if (!languageLabel) languageLabel = 'Unknown'
469 category: this.category,
471 licence: this.licence,
473 language: this.language,
476 description: this.description,
478 isLocal: this.isOwned(),
479 author: this.VideoChannel.Author.name,
480 duration: this.duration,
483 dislikes: this.dislikes,
484 tags: map<TagInstance, string>(this.Tags, 'name'),
485 thumbnailPath: this.getThumbnailPath(),
486 previewPath: this.getPreviewPath(),
487 embedPath: this.getEmbedPath(),
488 createdAt: this.createdAt,
489 updatedAt: this.updatedAt
495 toFormattedDetailsJSON = function (this: VideoInstance) {
498 if (this.VideoChannel.Author.Pod) {
499 podHost = this.VideoChannel.Author.Pod.host
501 // It means it's our video
502 podHost = CONFIG.WEBSERVER.HOST
505 // Maybe our pod is not up to date and there are new categories since our version
506 let categoryLabel = VIDEO_CATEGORIES[this.category]
507 if (!categoryLabel) categoryLabel = 'Misc'
509 // Maybe our pod is not up to date and there are new licences since our version
510 let licenceLabel = VIDEO_LICENCES[this.licence]
511 if (!licenceLabel) licenceLabel = 'Unknown'
513 // Language is an optional attribute
514 let languageLabel = VIDEO_LANGUAGES[this.language]
515 if (!languageLabel) languageLabel = 'Unknown'
521 category: this.category,
523 licence: this.licence,
525 language: this.language,
528 description: this.description,
530 isLocal: this.isOwned(),
531 author: this.VideoChannel.Author.name,
532 duration: this.duration,
535 dislikes: this.dislikes,
536 tags: map<TagInstance, string>(this.Tags, 'name'),
537 thumbnailPath: this.getThumbnailPath(),
538 previewPath: this.getPreviewPath(),
539 embedPath: this.getEmbedPath(),
540 createdAt: this.createdAt,
541 updatedAt: this.updatedAt,
542 channel: this.VideoChannel.toFormattedJSON(),
546 // Format and sort video files
547 const { baseUrlHttp, baseUrlWs } = getBaseUrls(this)
548 json.files = this.VideoFiles
550 let resolutionLabel = videoFile.resolution + 'p'
552 const videoFileJson = {
553 resolution: videoFile.resolution,
555 magnetUri: generateMagnetUri(this, videoFile, baseUrlHttp, baseUrlWs),
556 size: videoFile.size,
557 torrentUrl: getTorrentUrl(this, videoFile, baseUrlHttp),
558 fileUrl: getVideoFileUrl(this, videoFile, baseUrlHttp)
564 if (a.resolution < b.resolution) return 1
565 if (a.resolution === b.resolution) return 0
572 toAddRemoteJSON = function (this: VideoInstance) {
573 // Get thumbnail data to send to the other pod
574 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
576 return readFileBufferPromise(thumbnailPath).then(thumbnailData => {
577 const remoteVideo = {
580 category: this.category,
581 licence: this.licence,
582 language: this.language,
584 description: this.description,
585 channelUUID: this.VideoChannel.uuid,
586 duration: this.duration,
587 thumbnailData: thumbnailData.toString('binary'),
588 tags: map<TagInstance, string>(this.Tags, 'name'),
589 createdAt: this.createdAt,
590 updatedAt: this.updatedAt,
593 dislikes: this.dislikes,
597 this.VideoFiles.forEach(videoFile => {
598 remoteVideo.files.push({
599 infoHash: videoFile.infoHash,
600 resolution: videoFile.resolution,
601 extname: videoFile.extname,
610 toUpdateRemoteJSON = function (this: VideoInstance) {
614 category: this.category,
615 licence: this.licence,
616 language: this.language,
618 description: this.description,
619 duration: this.duration,
620 tags: map<TagInstance, string>(this.Tags, 'name'),
621 createdAt: this.createdAt,
622 updatedAt: this.updatedAt,
625 dislikes: this.dislikes,
629 this.VideoFiles.forEach(videoFile => {
631 infoHash: videoFile.infoHash,
632 resolution: videoFile.resolution,
633 extname: videoFile.extname,
641 optimizeOriginalVideofile = function (this: VideoInstance) {
642 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
643 const newExtname = '.mp4'
644 const inputVideoFile = this.getOriginalFile()
645 const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
646 const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
648 const transcodeOptions = {
649 inputPath: videoInputPath,
650 outputPath: videoOutputPath
653 return transcode(transcodeOptions)
655 return unlinkPromise(videoInputPath)
658 // Important to do this before getVideoFilename() to take in account the new file extension
659 inputVideoFile.set('extname', newExtname)
661 return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile))
664 return statPromise(this.getVideoFilePath(inputVideoFile))
667 return inputVideoFile.set('size', stats.size)
670 return this.createTorrentAndSetInfoHash(inputVideoFile)
673 return inputVideoFile.save()
679 // Auto destruction...
680 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
686 transcodeOriginalVideofile = function (this: VideoInstance, resolution: VideoResolution) {
687 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
688 const extname = '.mp4'
690 // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
691 const videoInputPath = join(videosDirectory, this.getVideoFilename(this.getOriginalFile()))
693 const newVideoFile = (Video['sequelize'].models.VideoFile as VideoFileModel).build({
699 const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile))
701 const transcodeOptions = {
702 inputPath: videoInputPath,
703 outputPath: videoOutputPath,
706 return transcode(transcodeOptions)
708 return statPromise(videoOutputPath)
711 newVideoFile.set('size', stats.size)
716 return this.createTorrentAndSetInfoHash(newVideoFile)
719 return newVideoFile.save()
722 return this.VideoFiles.push(newVideoFile)
724 .then(() => undefined)
727 getOriginalFileHeight = function (this: VideoInstance) {
728 const originalFilePath = this.getVideoFilePath(this.getOriginalFile())
730 return getVideoFileHeight(originalFilePath)
733 removeThumbnail = function (this: VideoInstance) {
734 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
735 return unlinkPromise(thumbnailPath)
738 removePreview = function (this: VideoInstance) {
739 // Same name than video thumbnail
740 return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName())
743 removeFile = function (this: VideoInstance, videoFile: VideoFileInstance) {
744 const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
745 return unlinkPromise(filePath)
748 removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) {
749 const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
750 return unlinkPromise(torrentPath)
753 // ------------------------------ STATICS ------------------------------
755 generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) {
756 // Creating the thumbnail for a remote video
758 const thumbnailName = video.getThumbnailName()
759 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
760 return writeFilePromise(thumbnailPath, Buffer.from(thumbnailData, 'binary')).then(() => {
767 include: [ Video['sequelize'].models.VideoFile ]
770 return Video.findAll(query)
773 listForApi = function (start: number, count: number, sort: string) {
774 // Exclude blacklisted videos from the list
779 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
782 model: Video['sequelize'].models.VideoChannel,
785 model: Video['sequelize'].models.Author,
788 model: Video['sequelize'].models.Pod,
795 Video['sequelize'].models.Tag,
796 Video['sequelize'].models.VideoFile
798 where: createBaseVideosWhere()
801 return Video.findAndCountAll(query).then(({ rows, count }) => {
809 loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Transaction) {
810 const query: Sequelize.FindOptions<VideoAttributes> = {
816 model: Video['sequelize'].models.VideoFile
819 model: Video['sequelize'].models.VideoChannel,
822 model: Video['sequelize'].models.Author,
825 model: Video['sequelize'].models.Pod,
838 if (t !== undefined) query.transaction = t
840 return Video.findOne(query)
843 listOwnedAndPopulateAuthorAndTags = function () {
849 Video['sequelize'].models.VideoFile,
851 model: Video['sequelize'].models.VideoChannel,
852 include: [ Video['sequelize'].models.Author ]
854 Video['sequelize'].models.Tag
858 return Video.findAll(query)
861 listOwnedByAuthor = function (author: string) {
868 model: Video['sequelize'].models.VideoFile
871 model: Video['sequelize'].models.VideoChannel,
874 model: Video['sequelize'].models.Author,
884 return Video.findAll(query)
887 load = function (id: number) {
888 return Video.findById(id)
891 loadByUUID = function (uuid: string, t?: Sequelize.Transaction) {
892 const query: Sequelize.FindOptions<VideoAttributes> = {
896 include: [ Video['sequelize'].models.VideoFile ]
899 if (t !== undefined) query.transaction = t
901 return Video.findOne(query)
904 loadLocalVideoByUUID = function (uuid: string, t?: Sequelize.Transaction) {
905 const query: Sequelize.FindOptions<VideoAttributes> = {
910 include: [ Video['sequelize'].models.VideoFile ]
913 if (t !== undefined) query.transaction = t
915 return Video.findOne(query)
918 loadAndPopulateAuthor = function (id: number) {
921 Video['sequelize'].models.VideoFile,
923 model: Video['sequelize'].models.VideoChannel,
924 include: [ Video['sequelize'].models.Author ]
929 return Video.findById(id, options)
932 loadAndPopulateAuthorAndPodAndTags = function (id: number) {
936 model: Video['sequelize'].models.VideoChannel,
939 model: Video['sequelize'].models.Author,
940 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
944 Video['sequelize'].models.Tag,
945 Video['sequelize'].models.VideoFile
949 return Video.findById(id, options)
952 loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) {
959 model: Video['sequelize'].models.VideoChannel,
962 model: Video['sequelize'].models.Author,
963 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
967 Video['sequelize'].models.Tag,
968 Video['sequelize'].models.VideoFile
972 return Video.findOne(options)
975 searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, start: number, count: number, sort: string) {
976 const podInclude: Sequelize.IncludeOptions = {
977 model: Video['sequelize'].models.Pod,
981 const authorInclude: Sequelize.IncludeOptions = {
982 model: Video['sequelize'].models.Author,
983 include: [ podInclude ]
986 const videoChannelInclude: Sequelize.IncludeOptions = {
987 model: Video['sequelize'].models.VideoChannel,
988 include: [ authorInclude ],
992 const tagInclude: Sequelize.IncludeOptions = {
993 model: Video['sequelize'].models.Tag
996 const videoFileInclude: Sequelize.IncludeOptions = {
997 model: Video['sequelize'].models.VideoFile
1000 const query: Sequelize.FindOptions<VideoAttributes> = {
1002 where: createBaseVideosWhere(),
1005 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ]
1008 // Make an exact search with the magnet
1009 if (field === 'magnetUri') {
1010 videoFileInclude.where = {
1011 infoHash: magnetUtil.decode(value).infoHash
1013 } else if (field === 'tags') {
1014 const escapedValue = Video['sequelize'].escape('%' + value + '%')
1015 query.where['id'].$in = Video['sequelize'].literal(
1016 `(SELECT "VideoTags"."videoId"
1018 INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId"
1019 WHERE name ILIKE ${escapedValue}
1022 } else if (field === 'host') {
1023 // FIXME: Include our pod? (not stored in the database)
1024 podInclude.where = {
1026 $iLike: '%' + value + '%'
1029 podInclude.required = true
1030 } else if (field === 'author') {
1031 authorInclude.where = {
1033 $iLike: '%' + value + '%'
1037 query.where[field] = {
1038 $iLike: '%' + value + '%'
1043 videoChannelInclude, tagInclude, videoFileInclude
1046 return Video.findAndCountAll(query).then(({ rows, count }) => {
1054 // ---------------------------------------------------------------------------
1056 function createBaseVideosWhere () {
1059 $notIn: Video['sequelize'].literal(
1060 '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")'
1066 function getBaseUrls (video: VideoInstance) {
1070 if (video.isOwned()) {
1071 baseUrlHttp = CONFIG.WEBSERVER.URL
1072 baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
1074 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + video.VideoChannel.Author.Pod.host
1075 baseUrlWs = REMOTE_SCHEME.WS + '://' + video.VideoChannel.Author.Pod.host
1078 return { baseUrlHttp, baseUrlWs }
1081 function getTorrentUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) {
1082 return baseUrlHttp + STATIC_PATHS.TORRENTS + video.getTorrentFileName(videoFile)
1085 function getVideoFileUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) {
1086 return baseUrlHttp + STATIC_PATHS.WEBSEED + video.getVideoFilename(videoFile)
1089 function generateMagnetUri (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string, baseUrlWs: string) {
1090 const xs = getTorrentUrl(video, videoFile, baseUrlHttp)
1091 const announce = [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
1092 const urlList = [ getVideoFileUrl(video, videoFile, baseUrlHttp) ]
1094 const magnetHash = {
1098 infoHash: videoFile.infoHash,
1102 return magnetUtil.encode(magnetHash)