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 loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor
84 let loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags
85 let loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags
86 let searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags
87 let removeThumbnail: VideoMethods.RemoveThumbnail
88 let removePreview: VideoMethods.RemovePreview
89 let removeFile: VideoMethods.RemoveFile
90 let removeTorrent: VideoMethods.RemoveTorrent
92 export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
93 Video = sequelize.define<VideoInstance, VideoAttributes>('Video',
97 defaultValue: DataTypes.UUIDV4,
104 type: DataTypes.STRING,
107 nameValid: value => {
108 const res = isVideoNameValid(value)
109 if (res === false) throw new Error('Video name is not valid.')
114 type: DataTypes.INTEGER,
117 categoryValid: value => {
118 const res = isVideoCategoryValid(value)
119 if (res === false) throw new Error('Video category is not valid.')
124 type: DataTypes.INTEGER,
128 licenceValid: value => {
129 const res = isVideoLicenceValid(value)
130 if (res === false) throw new Error('Video licence is not valid.')
135 type: DataTypes.INTEGER,
138 languageValid: value => {
139 const res = isVideoLanguageValid(value)
140 if (res === false) throw new Error('Video language is not valid.')
145 type: DataTypes.BOOLEAN,
148 nsfwValid: value => {
149 const res = isVideoNSFWValid(value)
150 if (res === false) throw new Error('Video nsfw attribute is not valid.')
155 type: DataTypes.STRING,
158 descriptionValid: value => {
159 const res = isVideoDescriptionValid(value)
160 if (res === false) throw new Error('Video description is not valid.')
165 type: DataTypes.INTEGER,
168 durationValid: value => {
169 const res = isVideoDurationValid(value)
170 if (res === false) throw new Error('Video duration is not valid.')
175 type: DataTypes.INTEGER,
184 type: DataTypes.INTEGER,
193 type: DataTypes.INTEGER,
202 type: DataTypes.BOOLEAN,
213 fields: [ 'createdAt' ]
216 fields: [ 'duration' ]
228 fields: [ 'channelId' ]
237 const classMethods = [
240 generateThumbnailFromData,
243 listOwnedAndPopulateAuthorAndTags,
246 loadAndPopulateAuthor,
247 loadAndPopulateAuthorAndPodAndTags,
250 loadByUUIDAndPopulateAuthorAndPodAndTags,
251 searchAndPopulateAuthorAndPodAndTags
253 const instanceMethods = [
256 createTorrentAndSetInfoHash,
272 toFormattedDetailsJSON,
274 optimizeOriginalVideofile,
275 transcodeOriginalVideofile,
276 getOriginalFileHeight,
279 addMethodsToModel(Video, classMethods, instanceMethods)
284 // ------------------------------ METHODS ------------------------------
286 function associate (models) {
287 Video.belongsTo(models.VideoChannel, {
295 Video.belongsToMany(models.Tag, {
296 foreignKey: 'videoId',
297 through: models.VideoTag,
301 Video.hasMany(models.VideoAbuse, {
309 Video.hasMany(models.VideoFile, {
318 function afterDestroy (video: VideoInstance, options: { transaction: Sequelize.Transaction }) {
322 video.removeThumbnail()
325 if (video.isOwned()) {
326 const removeVideoToFriendsParams = {
331 video.removePreview(),
332 removeVideoToFriends(removeVideoToFriendsParams, options.transaction)
335 // Remove physical files and torrents
336 video.VideoFiles.forEach(file => {
337 tasks.push(video.removeFile(file))
338 tasks.push(video.removeTorrent(file))
342 return Promise.all(tasks)
344 logger.error('Some errors when removing files of video %d in after destroy hook.', video.uuid, err)
348 getOriginalFile = function (this: VideoInstance) {
349 if (Array.isArray(this.VideoFiles) === false) return undefined
351 // The original file is the file that have the higher resolution
352 return maxBy(this.VideoFiles, file => file.resolution)
355 getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) {
356 return this.uuid + '-' + videoFile.resolution + videoFile.extname
359 getThumbnailName = function (this: VideoInstance) {
360 // We always have a copy of the thumbnail
361 const extension = '.jpg'
362 return this.uuid + extension
365 getPreviewName = function (this: VideoInstance) {
366 const extension = '.jpg'
367 return this.uuid + extension
370 getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) {
371 const extension = '.torrent'
372 return this.uuid + '-' + videoFile.resolution + extension
375 isOwned = function (this: VideoInstance) {
376 return this.remote === false
379 createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) {
380 const imageSize = PREVIEWS_SIZE.width + 'x' + PREVIEWS_SIZE.height
382 return generateImageFromVideoFile(
383 this.getVideoFilePath(videoFile),
384 CONFIG.STORAGE.PREVIEWS_DIR,
385 this.getPreviewName(),
390 createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) {
391 const imageSize = THUMBNAILS_SIZE.width + 'x' + THUMBNAILS_SIZE.height
393 return generateImageFromVideoFile(
394 this.getVideoFilePath(videoFile),
395 CONFIG.STORAGE.THUMBNAILS_DIR,
396 this.getThumbnailName(),
401 getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance) {
402 return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
405 createTorrentAndSetInfoHash = function (this: VideoInstance, videoFile: VideoFileInstance) {
408 [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
411 CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
415 return createTorrentPromise(this.getVideoFilePath(videoFile), options)
417 const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
418 logger.info('Creating torrent %s.', filePath)
420 return writeFilePromise(filePath, torrent).then(() => torrent)
423 const parsedTorrent = parseTorrent(torrent)
425 videoFile.infoHash = parsedTorrent.infoHash
429 getEmbedPath = function (this: VideoInstance) {
430 return '/videos/embed/' + this.uuid
433 getThumbnailPath = function (this: VideoInstance) {
434 return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName())
437 getPreviewPath = function (this: VideoInstance) {
438 return join(STATIC_PATHS.PREVIEWS, this.getPreviewName())
441 toFormattedJSON = function (this: VideoInstance) {
444 if (this.VideoChannel.Author.Pod) {
445 podHost = this.VideoChannel.Author.Pod.host
447 // It means it's our video
448 podHost = CONFIG.WEBSERVER.HOST
451 // Maybe our pod is not up to date and there are new categories since our version
452 let categoryLabel = VIDEO_CATEGORIES[this.category]
453 if (!categoryLabel) categoryLabel = 'Misc'
455 // Maybe our pod is not up to date and there are new licences since our version
456 let licenceLabel = VIDEO_LICENCES[this.licence]
457 if (!licenceLabel) licenceLabel = 'Unknown'
459 // Language is an optional attribute
460 let languageLabel = VIDEO_LANGUAGES[this.language]
461 if (!languageLabel) languageLabel = 'Unknown'
467 category: this.category,
469 licence: this.licence,
471 language: this.language,
474 description: this.description,
476 isLocal: this.isOwned(),
477 author: this.VideoChannel.Author.name,
478 duration: this.duration,
481 dislikes: this.dislikes,
482 tags: map<TagInstance, string>(this.Tags, 'name'),
483 thumbnailPath: this.getThumbnailPath(),
484 previewPath: this.getPreviewPath(),
485 embedPath: this.getEmbedPath(),
486 createdAt: this.createdAt,
487 updatedAt: this.updatedAt
493 toFormattedDetailsJSON = function (this: VideoInstance) {
496 if (this.VideoChannel.Author.Pod) {
497 podHost = this.VideoChannel.Author.Pod.host
499 // It means it's our video
500 podHost = CONFIG.WEBSERVER.HOST
503 // Maybe our pod is not up to date and there are new categories since our version
504 let categoryLabel = VIDEO_CATEGORIES[this.category]
505 if (!categoryLabel) categoryLabel = 'Misc'
507 // Maybe our pod is not up to date and there are new licences since our version
508 let licenceLabel = VIDEO_LICENCES[this.licence]
509 if (!licenceLabel) licenceLabel = 'Unknown'
511 // Language is an optional attribute
512 let languageLabel = VIDEO_LANGUAGES[this.language]
513 if (!languageLabel) languageLabel = 'Unknown'
519 category: this.category,
521 licence: this.licence,
523 language: this.language,
526 description: this.description,
528 isLocal: this.isOwned(),
529 author: this.VideoChannel.Author.name,
530 duration: this.duration,
533 dislikes: this.dislikes,
534 tags: map<TagInstance, string>(this.Tags, 'name'),
535 thumbnailPath: this.getThumbnailPath(),
536 previewPath: this.getPreviewPath(),
537 embedPath: this.getEmbedPath(),
538 createdAt: this.createdAt,
539 updatedAt: this.updatedAt,
540 channel: this.VideoChannel.toFormattedJSON(),
544 // Format and sort video files
545 const { baseUrlHttp, baseUrlWs } = getBaseUrls(this)
546 json.files = this.VideoFiles
548 let resolutionLabel = videoFile.resolution + 'p'
550 const videoFileJson = {
551 resolution: videoFile.resolution,
553 magnetUri: generateMagnetUri(this, videoFile, baseUrlHttp, baseUrlWs),
554 size: videoFile.size,
555 torrentUrl: getTorrentUrl(this, videoFile, baseUrlHttp),
556 fileUrl: getVideoFileUrl(this, videoFile, baseUrlHttp)
562 if (a.resolution < b.resolution) return 1
563 if (a.resolution === b.resolution) return 0
570 toAddRemoteJSON = function (this: VideoInstance) {
571 // Get thumbnail data to send to the other pod
572 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
574 return readFileBufferPromise(thumbnailPath).then(thumbnailData => {
575 const remoteVideo = {
578 category: this.category,
579 licence: this.licence,
580 language: this.language,
582 description: this.description,
583 channelUUID: this.VideoChannel.uuid,
584 duration: this.duration,
585 thumbnailData: thumbnailData.toString('binary'),
586 tags: map<TagInstance, string>(this.Tags, 'name'),
587 createdAt: this.createdAt,
588 updatedAt: this.updatedAt,
591 dislikes: this.dislikes,
595 this.VideoFiles.forEach(videoFile => {
596 remoteVideo.files.push({
597 infoHash: videoFile.infoHash,
598 resolution: videoFile.resolution,
599 extname: videoFile.extname,
608 toUpdateRemoteJSON = function (this: VideoInstance) {
612 category: this.category,
613 licence: this.licence,
614 language: this.language,
616 description: this.description,
617 duration: this.duration,
618 tags: map<TagInstance, string>(this.Tags, 'name'),
619 createdAt: this.createdAt,
620 updatedAt: this.updatedAt,
623 dislikes: this.dislikes,
627 this.VideoFiles.forEach(videoFile => {
629 infoHash: videoFile.infoHash,
630 resolution: videoFile.resolution,
631 extname: videoFile.extname,
639 optimizeOriginalVideofile = function (this: VideoInstance) {
640 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
641 const newExtname = '.mp4'
642 const inputVideoFile = this.getOriginalFile()
643 const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
644 const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
646 const transcodeOptions = {
647 inputPath: videoInputPath,
648 outputPath: videoOutputPath
651 return transcode(transcodeOptions)
653 return unlinkPromise(videoInputPath)
656 // Important to do this before getVideoFilename() to take in account the new file extension
657 inputVideoFile.set('extname', newExtname)
659 return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile))
662 return statPromise(this.getVideoFilePath(inputVideoFile))
665 return inputVideoFile.set('size', stats.size)
668 return this.createTorrentAndSetInfoHash(inputVideoFile)
671 return inputVideoFile.save()
677 // Auto destruction...
678 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
684 transcodeOriginalVideofile = function (this: VideoInstance, resolution: VideoResolution) {
685 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
686 const extname = '.mp4'
688 // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
689 const videoInputPath = join(videosDirectory, this.getVideoFilename(this.getOriginalFile()))
691 const newVideoFile = (Video['sequelize'].models.VideoFile as VideoFileModel).build({
697 const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile))
699 const transcodeOptions = {
700 inputPath: videoInputPath,
701 outputPath: videoOutputPath,
704 return transcode(transcodeOptions)
706 return statPromise(videoOutputPath)
709 newVideoFile.set('size', stats.size)
714 return this.createTorrentAndSetInfoHash(newVideoFile)
717 return newVideoFile.save()
720 return this.VideoFiles.push(newVideoFile)
722 .then(() => undefined)
725 getOriginalFileHeight = function (this: VideoInstance) {
726 const originalFilePath = this.getVideoFilePath(this.getOriginalFile())
728 return getVideoFileHeight(originalFilePath)
731 removeThumbnail = function (this: VideoInstance) {
732 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
733 return unlinkPromise(thumbnailPath)
736 removePreview = function (this: VideoInstance) {
737 // Same name than video thumbnail
738 return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName())
741 removeFile = function (this: VideoInstance, videoFile: VideoFileInstance) {
742 const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
743 return unlinkPromise(filePath)
746 removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) {
747 const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
748 return unlinkPromise(torrentPath)
751 // ------------------------------ STATICS ------------------------------
753 generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) {
754 // Creating the thumbnail for a remote video
756 const thumbnailName = video.getThumbnailName()
757 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
758 return writeFilePromise(thumbnailPath, Buffer.from(thumbnailData, 'binary')).then(() => {
765 include: [ Video['sequelize'].models.VideoFile ]
768 return Video.findAll(query)
771 listForApi = function (start: number, count: number, sort: string) {
772 // Exclude blacklisted videos from the list
777 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
780 model: Video['sequelize'].models.VideoChannel,
783 model: Video['sequelize'].models.Author,
786 model: Video['sequelize'].models.Pod,
793 Video['sequelize'].models.Tag,
794 Video['sequelize'].models.VideoFile
796 where: createBaseVideosWhere()
799 return Video.findAndCountAll(query).then(({ rows, count }) => {
807 loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Transaction) {
808 const query: Sequelize.FindOptions<VideoAttributes> = {
814 model: Video['sequelize'].models.VideoFile
817 model: Video['sequelize'].models.VideoChannel,
820 model: Video['sequelize'].models.Author,
823 model: Video['sequelize'].models.Pod,
836 if (t !== undefined) query.transaction = t
838 return Video.findOne(query)
841 listOwnedAndPopulateAuthorAndTags = function () {
847 Video['sequelize'].models.VideoFile,
849 model: Video['sequelize'].models.VideoChannel,
850 include: [ Video['sequelize'].models.Author ]
852 Video['sequelize'].models.Tag
856 return Video.findAll(query)
859 listOwnedByAuthor = function (author: string) {
866 model: Video['sequelize'].models.VideoFile
869 model: Video['sequelize'].models.VideoChannel,
872 model: Video['sequelize'].models.Author,
882 return Video.findAll(query)
885 load = function (id: number) {
886 return Video.findById(id)
889 loadByUUID = function (uuid: string, t?: Sequelize.Transaction) {
890 const query: Sequelize.FindOptions<VideoAttributes> = {
894 include: [ Video['sequelize'].models.VideoFile ]
897 if (t !== undefined) query.transaction = t
899 return Video.findOne(query)
902 loadAndPopulateAuthor = function (id: number) {
905 Video['sequelize'].models.VideoFile,
907 model: Video['sequelize'].models.VideoChannel,
908 include: [ Video['sequelize'].models.Author ]
913 return Video.findById(id, options)
916 loadAndPopulateAuthorAndPodAndTags = function (id: number) {
920 model: Video['sequelize'].models.VideoChannel,
923 model: Video['sequelize'].models.Author,
924 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
928 Video['sequelize'].models.Tag,
929 Video['sequelize'].models.VideoFile
933 return Video.findById(id, options)
936 loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) {
943 model: Video['sequelize'].models.VideoChannel,
946 model: Video['sequelize'].models.Author,
947 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
951 Video['sequelize'].models.Tag,
952 Video['sequelize'].models.VideoFile
956 return Video.findOne(options)
959 searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, start: number, count: number, sort: string) {
960 const podInclude: Sequelize.IncludeOptions = {
961 model: Video['sequelize'].models.Pod,
965 const authorInclude: Sequelize.IncludeOptions = {
966 model: Video['sequelize'].models.Author,
967 include: [ podInclude ]
970 const videoChannelInclude: Sequelize.IncludeOptions = {
971 model: Video['sequelize'].models.VideoChannel,
972 include: [ authorInclude ],
976 const tagInclude: Sequelize.IncludeOptions = {
977 model: Video['sequelize'].models.Tag
980 const videoFileInclude: Sequelize.IncludeOptions = {
981 model: Video['sequelize'].models.VideoFile
984 const query: Sequelize.FindOptions<VideoAttributes> = {
986 where: createBaseVideosWhere(),
989 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ]
992 // Make an exact search with the magnet
993 if (field === 'magnetUri') {
994 videoFileInclude.where = {
995 infoHash: magnetUtil.decode(value).infoHash
997 } else if (field === 'tags') {
998 const escapedValue = Video['sequelize'].escape('%' + value + '%')
999 query.where['id'].$in = Video['sequelize'].literal(
1000 `(SELECT "VideoTags"."videoId"
1002 INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId"
1003 WHERE name ILIKE ${escapedValue}
1006 } else if (field === 'host') {
1007 // FIXME: Include our pod? (not stored in the database)
1008 podInclude.where = {
1010 $iLike: '%' + value + '%'
1013 podInclude.required = true
1014 } else if (field === 'author') {
1015 authorInclude.where = {
1017 $iLike: '%' + value + '%'
1021 query.where[field] = {
1022 $iLike: '%' + value + '%'
1027 videoChannelInclude, tagInclude, videoFileInclude
1030 return Video.findAndCountAll(query).then(({ rows, count }) => {
1038 // ---------------------------------------------------------------------------
1040 function createBaseVideosWhere () {
1043 $notIn: Video['sequelize'].literal(
1044 '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")'
1050 function getBaseUrls (video: VideoInstance) {
1054 if (video.isOwned()) {
1055 baseUrlHttp = CONFIG.WEBSERVER.URL
1056 baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
1058 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + video.VideoChannel.Author.Pod.host
1059 baseUrlWs = REMOTE_SCHEME.WS + '://' + video.VideoChannel.Author.Pod.host
1062 return { baseUrlHttp, baseUrlWs }
1065 function getTorrentUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) {
1066 return baseUrlHttp + STATIC_PATHS.TORRENTS + video.getTorrentFileName(videoFile)
1069 function getVideoFileUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) {
1070 return baseUrlHttp + STATIC_PATHS.WEBSEED + video.getVideoFilename(videoFile)
1073 function generateMagnetUri (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string, baseUrlWs: string) {
1074 const xs = getTorrentUrl(video, videoFile, baseUrlHttp)
1075 const announce = [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
1076 const urlList = [ getVideoFileUrl(video, videoFile, baseUrlHttp) ]
1078 const magnetHash = {
1082 infoHash: videoFile.infoHash,
1086 return magnetUtil.encode(magnetHash)