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, truncate } from 'lodash'
11 import { TagInstance } from './tag-interface'
19 isVideoDescriptionValid,
21 readFileBufferPromise,
27 generateImageFromVideoFile,
30 } from '../../helpers'
42 } from '../../initializers'
43 import { removeVideoToFriends } from '../../lib'
44 import { VideoResolution } from '../../../shared'
45 import { VideoFileInstance, VideoFileModel } from './video-file-interface'
47 import { addMethodsToModel, getSort } from '../utils'
53 } from './video-interface'
55 let Video: Sequelize.Model<VideoInstance, VideoAttributes>
56 let getOriginalFile: VideoMethods.GetOriginalFile
57 let getVideoFilename: VideoMethods.GetVideoFilename
58 let getThumbnailName: VideoMethods.GetThumbnailName
59 let getThumbnailPath: VideoMethods.GetThumbnailPath
60 let getPreviewName: VideoMethods.GetPreviewName
61 let getPreviewPath: VideoMethods.GetPreviewPath
62 let getTorrentFileName: VideoMethods.GetTorrentFileName
63 let isOwned: VideoMethods.IsOwned
64 let toFormattedJSON: VideoMethods.ToFormattedJSON
65 let toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON
66 let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
67 let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
68 let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
69 let transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
70 let createPreview: VideoMethods.CreatePreview
71 let createThumbnail: VideoMethods.CreateThumbnail
72 let getVideoFilePath: VideoMethods.GetVideoFilePath
73 let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
74 let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
75 let getEmbedPath: VideoMethods.GetEmbedPath
76 let getDescriptionPath: VideoMethods.GetDescriptionPath
77 let getTruncatedDescription: VideoMethods.GetTruncatedDescription
79 let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
80 let list: VideoMethods.List
81 let listForApi: VideoMethods.ListForApi
82 let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
83 let listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags
84 let listOwnedByAuthor: VideoMethods.ListOwnedByAuthor
85 let load: VideoMethods.Load
86 let loadByUUID: VideoMethods.LoadByUUID
87 let loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID
88 let loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor
89 let loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags
90 let loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags
91 let searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags
92 let removeThumbnail: VideoMethods.RemoveThumbnail
93 let removePreview: VideoMethods.RemovePreview
94 let removeFile: VideoMethods.RemoveFile
95 let removeTorrent: VideoMethods.RemoveTorrent
97 export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
98 Video = sequelize.define<VideoInstance, VideoAttributes>('Video',
101 type: DataTypes.UUID,
102 defaultValue: DataTypes.UUIDV4,
109 type: DataTypes.STRING,
112 nameValid: value => {
113 const res = isVideoNameValid(value)
114 if (res === false) throw new Error('Video name is not valid.')
119 type: DataTypes.INTEGER,
122 categoryValid: value => {
123 const res = isVideoCategoryValid(value)
124 if (res === false) throw new Error('Video category is not valid.')
129 type: DataTypes.INTEGER,
133 licenceValid: value => {
134 const res = isVideoLicenceValid(value)
135 if (res === false) throw new Error('Video licence is not valid.')
140 type: DataTypes.INTEGER,
143 languageValid: value => {
144 const res = isVideoLanguageValid(value)
145 if (res === false) throw new Error('Video language is not valid.')
150 type: DataTypes.BOOLEAN,
153 nsfwValid: value => {
154 const res = isVideoNSFWValid(value)
155 if (res === false) throw new Error('Video nsfw attribute is not valid.')
160 type: DataTypes.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max),
163 descriptionValid: value => {
164 const res = isVideoDescriptionValid(value)
165 if (res === false) throw new Error('Video description is not valid.')
170 type: DataTypes.INTEGER,
173 durationValid: value => {
174 const res = isVideoDurationValid(value)
175 if (res === false) throw new Error('Video duration is not valid.')
180 type: DataTypes.INTEGER,
189 type: DataTypes.INTEGER,
198 type: DataTypes.INTEGER,
207 type: DataTypes.BOOLEAN,
218 fields: [ 'createdAt' ]
221 fields: [ 'duration' ]
233 fields: [ 'channelId' ]
242 const classMethods = [
245 generateThumbnailFromData,
248 listOwnedAndPopulateAuthorAndTags,
251 loadAndPopulateAuthor,
252 loadAndPopulateAuthorAndPodAndTags,
255 loadLocalVideoByUUID,
256 loadByUUIDAndPopulateAuthorAndPodAndTags,
257 searchAndPopulateAuthorAndPodAndTags
259 const instanceMethods = [
262 createTorrentAndSetInfoHash,
278 toFormattedDetailsJSON,
280 optimizeOriginalVideofile,
281 transcodeOriginalVideofile,
282 getOriginalFileHeight,
284 getTruncatedDescription,
287 addMethodsToModel(Video, classMethods, instanceMethods)
292 // ------------------------------ METHODS ------------------------------
294 function associate (models) {
295 Video.belongsTo(models.VideoChannel, {
303 Video.belongsToMany(models.Tag, {
304 foreignKey: 'videoId',
305 through: models.VideoTag,
309 Video.hasMany(models.VideoAbuse, {
317 Video.hasMany(models.VideoFile, {
326 function afterDestroy (video: VideoInstance) {
330 video.removeThumbnail()
333 if (video.isOwned()) {
334 const removeVideoToFriendsParams = {
339 video.removePreview(),
340 removeVideoToFriends(removeVideoToFriendsParams)
343 // Remove physical files and torrents
344 video.VideoFiles.forEach(file => {
345 tasks.push(video.removeFile(file))
346 tasks.push(video.removeTorrent(file))
350 return Promise.all(tasks)
352 logger.error('Some errors when removing files of video %s in after destroy hook.', video.uuid, err)
356 getOriginalFile = function (this: VideoInstance) {
357 if (Array.isArray(this.VideoFiles) === false) return undefined
359 // The original file is the file that have the higher resolution
360 return maxBy(this.VideoFiles, file => file.resolution)
363 getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) {
364 return this.uuid + '-' + videoFile.resolution + videoFile.extname
367 getThumbnailName = function (this: VideoInstance) {
368 // We always have a copy of the thumbnail
369 const extension = '.jpg'
370 return this.uuid + extension
373 getPreviewName = function (this: VideoInstance) {
374 const extension = '.jpg'
375 return this.uuid + extension
378 getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) {
379 const extension = '.torrent'
380 return this.uuid + '-' + videoFile.resolution + extension
383 isOwned = function (this: VideoInstance) {
384 return this.remote === false
387 createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) {
388 const imageSize = PREVIEWS_SIZE.width + 'x' + PREVIEWS_SIZE.height
390 return generateImageFromVideoFile(
391 this.getVideoFilePath(videoFile),
392 CONFIG.STORAGE.PREVIEWS_DIR,
393 this.getPreviewName(),
398 createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) {
399 const imageSize = THUMBNAILS_SIZE.width + 'x' + THUMBNAILS_SIZE.height
401 return generateImageFromVideoFile(
402 this.getVideoFilePath(videoFile),
403 CONFIG.STORAGE.THUMBNAILS_DIR,
404 this.getThumbnailName(),
409 getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance) {
410 return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
413 createTorrentAndSetInfoHash = function (this: VideoInstance, videoFile: VideoFileInstance) {
416 [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
419 CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
423 return createTorrentPromise(this.getVideoFilePath(videoFile), options)
425 const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
426 logger.info('Creating torrent %s.', filePath)
428 return writeFilePromise(filePath, torrent).then(() => torrent)
431 const parsedTorrent = parseTorrent(torrent)
433 videoFile.infoHash = parsedTorrent.infoHash
437 getEmbedPath = function (this: VideoInstance) {
438 return '/videos/embed/' + this.uuid
441 getThumbnailPath = function (this: VideoInstance) {
442 return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName())
445 getPreviewPath = function (this: VideoInstance) {
446 return join(STATIC_PATHS.PREVIEWS, this.getPreviewName())
449 toFormattedJSON = function (this: VideoInstance) {
452 if (this.VideoChannel.Author.Pod) {
453 podHost = this.VideoChannel.Author.Pod.host
455 // It means it's our video
456 podHost = CONFIG.WEBSERVER.HOST
459 // Maybe our pod is not up to date and there are new categories since our version
460 let categoryLabel = VIDEO_CATEGORIES[this.category]
461 if (!categoryLabel) categoryLabel = 'Misc'
463 // Maybe our pod is not up to date and there are new licences since our version
464 let licenceLabel = VIDEO_LICENCES[this.licence]
465 if (!licenceLabel) licenceLabel = 'Unknown'
467 // Language is an optional attribute
468 let languageLabel = VIDEO_LANGUAGES[this.language]
469 if (!languageLabel) languageLabel = 'Unknown'
475 category: this.category,
477 licence: this.licence,
479 language: this.language,
482 description: this.getTruncatedDescription(),
484 isLocal: this.isOwned(),
485 author: this.VideoChannel.Author.name,
486 duration: this.duration,
489 dislikes: this.dislikes,
490 tags: map<TagInstance, string>(this.Tags, 'name'),
491 thumbnailPath: this.getThumbnailPath(),
492 previewPath: this.getPreviewPath(),
493 embedPath: this.getEmbedPath(),
494 createdAt: this.createdAt,
495 updatedAt: this.updatedAt
501 toFormattedDetailsJSON = function (this: VideoInstance) {
502 const formattedJson = this.toFormattedJSON()
504 const detailsJson = {
505 descriptionPath: this.getDescriptionPath(),
506 channel: this.VideoChannel.toFormattedJSON(),
510 // Format and sort video files
511 const { baseUrlHttp, baseUrlWs } = getBaseUrls(this)
512 detailsJson.files = this.VideoFiles
514 let resolutionLabel = videoFile.resolution + 'p'
516 const videoFileJson = {
517 resolution: videoFile.resolution,
519 magnetUri: generateMagnetUri(this, videoFile, baseUrlHttp, baseUrlWs),
520 size: videoFile.size,
521 torrentUrl: getTorrentUrl(this, videoFile, baseUrlHttp),
522 fileUrl: getVideoFileUrl(this, videoFile, baseUrlHttp)
528 if (a.resolution < b.resolution) return 1
529 if (a.resolution === b.resolution) return 0
533 return Object.assign(formattedJson, detailsJson)
536 toAddRemoteJSON = function (this: VideoInstance) {
537 // Get thumbnail data to send to the other pod
538 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
540 return readFileBufferPromise(thumbnailPath).then(thumbnailData => {
541 const remoteVideo = {
544 category: this.category,
545 licence: this.licence,
546 language: this.language,
548 truncatedDescription: this.getTruncatedDescription(),
549 channelUUID: this.VideoChannel.uuid,
550 duration: this.duration,
551 thumbnailData: thumbnailData.toString('binary'),
552 tags: map<TagInstance, string>(this.Tags, 'name'),
553 createdAt: this.createdAt,
554 updatedAt: this.updatedAt,
557 dislikes: this.dislikes,
561 this.VideoFiles.forEach(videoFile => {
562 remoteVideo.files.push({
563 infoHash: videoFile.infoHash,
564 resolution: videoFile.resolution,
565 extname: videoFile.extname,
574 toUpdateRemoteJSON = function (this: VideoInstance) {
578 category: this.category,
579 licence: this.licence,
580 language: this.language,
582 truncatedDescription: this.getTruncatedDescription(),
583 duration: this.duration,
584 tags: map<TagInstance, string>(this.Tags, 'name'),
585 createdAt: this.createdAt,
586 updatedAt: this.updatedAt,
589 dislikes: this.dislikes,
593 this.VideoFiles.forEach(videoFile => {
595 infoHash: videoFile.infoHash,
596 resolution: videoFile.resolution,
597 extname: videoFile.extname,
605 getTruncatedDescription = function (this: VideoInstance) {
607 length: CONSTRAINTS_FIELDS.VIDEOS.TRUNCATED_DESCRIPTION.max
610 return truncate(this.description, options)
613 optimizeOriginalVideofile = function (this: VideoInstance) {
614 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
615 const newExtname = '.mp4'
616 const inputVideoFile = this.getOriginalFile()
617 const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
618 const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
620 const transcodeOptions = {
621 inputPath: videoInputPath,
622 outputPath: videoOutputPath
625 return transcode(transcodeOptions)
627 return unlinkPromise(videoInputPath)
630 // Important to do this before getVideoFilename() to take in account the new file extension
631 inputVideoFile.set('extname', newExtname)
633 return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile))
636 return statPromise(this.getVideoFilePath(inputVideoFile))
639 return inputVideoFile.set('size', stats.size)
642 return this.createTorrentAndSetInfoHash(inputVideoFile)
645 return inputVideoFile.save()
651 // Auto destruction...
652 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
658 transcodeOriginalVideofile = function (this: VideoInstance, resolution: VideoResolution) {
659 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
660 const extname = '.mp4'
662 // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
663 const videoInputPath = join(videosDirectory, this.getVideoFilename(this.getOriginalFile()))
665 const newVideoFile = (Video['sequelize'].models.VideoFile as VideoFileModel).build({
671 const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile))
673 const transcodeOptions = {
674 inputPath: videoInputPath,
675 outputPath: videoOutputPath,
678 return transcode(transcodeOptions)
680 return statPromise(videoOutputPath)
683 newVideoFile.set('size', stats.size)
688 return this.createTorrentAndSetInfoHash(newVideoFile)
691 return newVideoFile.save()
694 return this.VideoFiles.push(newVideoFile)
696 .then(() => undefined)
699 getOriginalFileHeight = function (this: VideoInstance) {
700 const originalFilePath = this.getVideoFilePath(this.getOriginalFile())
702 return getVideoFileHeight(originalFilePath)
705 getDescriptionPath = function (this: VideoInstance) {
706 return `/api/${API_VERSION}/videos/${this.uuid}/description`
709 removeThumbnail = function (this: VideoInstance) {
710 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
711 return unlinkPromise(thumbnailPath)
714 removePreview = function (this: VideoInstance) {
715 // Same name than video thumbnail
716 return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName())
719 removeFile = function (this: VideoInstance, videoFile: VideoFileInstance) {
720 const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
721 return unlinkPromise(filePath)
724 removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) {
725 const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
726 return unlinkPromise(torrentPath)
729 // ------------------------------ STATICS ------------------------------
731 generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) {
732 // Creating the thumbnail for a remote video
734 const thumbnailName = video.getThumbnailName()
735 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
736 return writeFilePromise(thumbnailPath, Buffer.from(thumbnailData, 'binary')).then(() => {
743 include: [ Video['sequelize'].models.VideoFile ]
746 return Video.findAll(query)
749 listForApi = function (start: number, count: number, sort: string) {
750 // Exclude blacklisted videos from the list
755 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
758 model: Video['sequelize'].models.VideoChannel,
761 model: Video['sequelize'].models.Author,
764 model: Video['sequelize'].models.Pod,
771 Video['sequelize'].models.Tag,
772 Video['sequelize'].models.VideoFile
774 where: createBaseVideosWhere()
777 return Video.findAndCountAll(query).then(({ rows, count }) => {
785 loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Transaction) {
786 const query: Sequelize.FindOptions<VideoAttributes> = {
792 model: Video['sequelize'].models.VideoFile
795 model: Video['sequelize'].models.VideoChannel,
798 model: Video['sequelize'].models.Author,
801 model: Video['sequelize'].models.Pod,
814 if (t !== undefined) query.transaction = t
816 return Video.findOne(query)
819 listOwnedAndPopulateAuthorAndTags = function () {
825 Video['sequelize'].models.VideoFile,
827 model: Video['sequelize'].models.VideoChannel,
828 include: [ Video['sequelize'].models.Author ]
830 Video['sequelize'].models.Tag
834 return Video.findAll(query)
837 listOwnedByAuthor = function (author: string) {
844 model: Video['sequelize'].models.VideoFile
847 model: Video['sequelize'].models.VideoChannel,
850 model: Video['sequelize'].models.Author,
860 return Video.findAll(query)
863 load = function (id: number) {
864 return Video.findById(id)
867 loadByUUID = function (uuid: string, t?: Sequelize.Transaction) {
868 const query: Sequelize.FindOptions<VideoAttributes> = {
872 include: [ Video['sequelize'].models.VideoFile ]
875 if (t !== undefined) query.transaction = t
877 return Video.findOne(query)
880 loadLocalVideoByUUID = function (uuid: string, t?: Sequelize.Transaction) {
881 const query: Sequelize.FindOptions<VideoAttributes> = {
886 include: [ Video['sequelize'].models.VideoFile ]
889 if (t !== undefined) query.transaction = t
891 return Video.findOne(query)
894 loadAndPopulateAuthor = function (id: number) {
897 Video['sequelize'].models.VideoFile,
899 model: Video['sequelize'].models.VideoChannel,
900 include: [ Video['sequelize'].models.Author ]
905 return Video.findById(id, options)
908 loadAndPopulateAuthorAndPodAndTags = function (id: number) {
912 model: Video['sequelize'].models.VideoChannel,
915 model: Video['sequelize'].models.Author,
916 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
920 Video['sequelize'].models.Tag,
921 Video['sequelize'].models.VideoFile
925 return Video.findById(id, options)
928 loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) {
935 model: Video['sequelize'].models.VideoChannel,
938 model: Video['sequelize'].models.Author,
939 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
943 Video['sequelize'].models.Tag,
944 Video['sequelize'].models.VideoFile
948 return Video.findOne(options)
951 searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, start: number, count: number, sort: string) {
952 const podInclude: Sequelize.IncludeOptions = {
953 model: Video['sequelize'].models.Pod,
957 const authorInclude: Sequelize.IncludeOptions = {
958 model: Video['sequelize'].models.Author,
959 include: [ podInclude ]
962 const videoChannelInclude: Sequelize.IncludeOptions = {
963 model: Video['sequelize'].models.VideoChannel,
964 include: [ authorInclude ],
968 const tagInclude: Sequelize.IncludeOptions = {
969 model: Video['sequelize'].models.Tag
972 const videoFileInclude: Sequelize.IncludeOptions = {
973 model: Video['sequelize'].models.VideoFile
976 const query: Sequelize.FindOptions<VideoAttributes> = {
978 where: createBaseVideosWhere(),
981 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ]
984 // Make an exact search with the magnet
985 if (field === 'magnetUri') {
986 videoFileInclude.where = {
987 infoHash: magnetUtil.decode(value).infoHash
989 } else if (field === 'tags') {
990 const escapedValue = Video['sequelize'].escape('%' + value + '%')
991 query.where['id'][Sequelize.Op.in] = Video['sequelize'].literal(
992 `(SELECT "VideoTags"."videoId"
994 INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId"
995 WHERE name ILIKE ${escapedValue}
998 } else if (field === 'host') {
999 // FIXME: Include our pod? (not stored in the database)
1000 podInclude.where = {
1002 [Sequelize.Op.iLike]: '%' + value + '%'
1005 podInclude.required = true
1006 } else if (field === 'author') {
1007 authorInclude.where = {
1009 [Sequelize.Op.iLike]: '%' + value + '%'
1013 query.where[field] = {
1014 [Sequelize.Op.iLike]: '%' + value + '%'
1019 videoChannelInclude, tagInclude, videoFileInclude
1022 return Video.findAndCountAll(query).then(({ rows, count }) => {
1030 // ---------------------------------------------------------------------------
1032 function createBaseVideosWhere () {
1035 [Sequelize.Op.notIn]: Video['sequelize'].literal(
1036 '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")'
1042 function getBaseUrls (video: VideoInstance) {
1046 if (video.isOwned()) {
1047 baseUrlHttp = CONFIG.WEBSERVER.URL
1048 baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
1050 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + video.VideoChannel.Author.Pod.host
1051 baseUrlWs = REMOTE_SCHEME.WS + '://' + video.VideoChannel.Author.Pod.host
1054 return { baseUrlHttp, baseUrlWs }
1057 function getTorrentUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) {
1058 return baseUrlHttp + STATIC_PATHS.TORRENTS + video.getTorrentFileName(videoFile)
1061 function getVideoFileUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) {
1062 return baseUrlHttp + STATIC_PATHS.WEBSEED + video.getVideoFilename(videoFile)
1065 function generateMagnetUri (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string, baseUrlWs: string) {
1066 const xs = getTorrentUrl(video, videoFile, baseUrlHttp)
1067 const announce = [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
1068 const urlList = [ getVideoFileUrl(video, videoFile, baseUrlHttp) ]
1070 const magnetHash = {
1074 infoHash: videoFile.infoHash,
1078 return magnetUtil.encode(magnetHash)