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,
22 readFileBufferPromise,
28 generateImageFromVideoFile,
31 } from '../../helpers'
44 } from '../../initializers'
45 import { removeVideoToFriends } from '../../lib'
46 import { VideoResolution, VideoPrivacy } from '../../../shared'
47 import { VideoFileInstance, VideoFileModel } from './video-file-interface'
49 import { addMethodsToModel, getSort } from '../utils'
55 } from './video-interface'
57 let Video: Sequelize.Model<VideoInstance, VideoAttributes>
58 let getOriginalFile: VideoMethods.GetOriginalFile
59 let getVideoFilename: VideoMethods.GetVideoFilename
60 let getThumbnailName: VideoMethods.GetThumbnailName
61 let getThumbnailPath: VideoMethods.GetThumbnailPath
62 let getPreviewName: VideoMethods.GetPreviewName
63 let getPreviewPath: VideoMethods.GetPreviewPath
64 let getTorrentFileName: VideoMethods.GetTorrentFileName
65 let isOwned: VideoMethods.IsOwned
66 let toFormattedJSON: VideoMethods.ToFormattedJSON
67 let toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON
68 let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON
69 let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON
70 let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
71 let transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
72 let createPreview: VideoMethods.CreatePreview
73 let createThumbnail: VideoMethods.CreateThumbnail
74 let getVideoFilePath: VideoMethods.GetVideoFilePath
75 let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
76 let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
77 let getEmbedPath: VideoMethods.GetEmbedPath
78 let getDescriptionPath: VideoMethods.GetDescriptionPath
79 let getTruncatedDescription: VideoMethods.GetTruncatedDescription
81 let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
82 let list: VideoMethods.List
83 let listForApi: VideoMethods.ListForApi
84 let listUserVideosForApi: VideoMethods.ListUserVideosForApi
85 let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
86 let listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags
87 let listOwnedByAuthor: VideoMethods.ListOwnedByAuthor
88 let load: VideoMethods.Load
89 let loadByUUID: VideoMethods.LoadByUUID
90 let loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID
91 let loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor
92 let loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags
93 let loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags
94 let searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags
95 let removeThumbnail: VideoMethods.RemoveThumbnail
96 let removePreview: VideoMethods.RemovePreview
97 let removeFile: VideoMethods.RemoveFile
98 let removeTorrent: VideoMethods.RemoveTorrent
100 export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
101 Video = sequelize.define<VideoInstance, VideoAttributes>('Video',
104 type: DataTypes.UUID,
105 defaultValue: DataTypes.UUIDV4,
112 type: DataTypes.STRING,
115 nameValid: value => {
116 const res = isVideoNameValid(value)
117 if (res === false) throw new Error('Video name is not valid.')
122 type: DataTypes.INTEGER,
125 categoryValid: value => {
126 const res = isVideoCategoryValid(value)
127 if (res === false) throw new Error('Video category is not valid.')
132 type: DataTypes.INTEGER,
136 licenceValid: value => {
137 const res = isVideoLicenceValid(value)
138 if (res === false) throw new Error('Video licence is not valid.')
143 type: DataTypes.INTEGER,
146 languageValid: value => {
147 const res = isVideoLanguageValid(value)
148 if (res === false) throw new Error('Video language is not valid.')
153 type: DataTypes.INTEGER,
156 privacyValid: value => {
157 const res = isVideoPrivacyValid(value)
158 if (res === false) throw new Error('Video privacy is not valid.')
163 type: DataTypes.BOOLEAN,
166 nsfwValid: value => {
167 const res = isVideoNSFWValid(value)
168 if (res === false) throw new Error('Video nsfw attribute is not valid.')
173 type: DataTypes.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max),
176 descriptionValid: value => {
177 const res = isVideoDescriptionValid(value)
178 if (res === false) throw new Error('Video description is not valid.')
183 type: DataTypes.INTEGER,
186 durationValid: value => {
187 const res = isVideoDurationValid(value)
188 if (res === false) throw new Error('Video duration is not valid.')
193 type: DataTypes.INTEGER,
202 type: DataTypes.INTEGER,
211 type: DataTypes.INTEGER,
220 type: DataTypes.BOOLEAN,
231 fields: [ 'createdAt' ]
234 fields: [ 'duration' ]
246 fields: [ 'channelId' ]
255 const classMethods = [
258 generateThumbnailFromData,
261 listUserVideosForApi,
262 listOwnedAndPopulateAuthorAndTags,
265 loadAndPopulateAuthor,
266 loadAndPopulateAuthorAndPodAndTags,
269 loadLocalVideoByUUID,
270 loadByUUIDAndPopulateAuthorAndPodAndTags,
271 searchAndPopulateAuthorAndPodAndTags
273 const instanceMethods = [
276 createTorrentAndSetInfoHash,
292 toFormattedDetailsJSON,
294 optimizeOriginalVideofile,
295 transcodeOriginalVideofile,
296 getOriginalFileHeight,
298 getTruncatedDescription,
301 addMethodsToModel(Video, classMethods, instanceMethods)
306 // ------------------------------ METHODS ------------------------------
308 function associate (models) {
309 Video.belongsTo(models.VideoChannel, {
317 Video.belongsToMany(models.Tag, {
318 foreignKey: 'videoId',
319 through: models.VideoTag,
323 Video.hasMany(models.VideoAbuse, {
331 Video.hasMany(models.VideoFile, {
340 function afterDestroy (video: VideoInstance) {
344 video.removeThumbnail()
347 if (video.isOwned()) {
348 const removeVideoToFriendsParams = {
353 video.removePreview(),
354 removeVideoToFriends(removeVideoToFriendsParams)
357 // Remove physical files and torrents
358 video.VideoFiles.forEach(file => {
359 tasks.push(video.removeFile(file))
360 tasks.push(video.removeTorrent(file))
364 return Promise.all(tasks)
366 logger.error('Some errors when removing files of video %s in after destroy hook.', video.uuid, err)
370 getOriginalFile = function (this: VideoInstance) {
371 if (Array.isArray(this.VideoFiles) === false) return undefined
373 // The original file is the file that have the higher resolution
374 return maxBy(this.VideoFiles, file => file.resolution)
377 getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) {
378 return this.uuid + '-' + videoFile.resolution + videoFile.extname
381 getThumbnailName = function (this: VideoInstance) {
382 // We always have a copy of the thumbnail
383 const extension = '.jpg'
384 return this.uuid + extension
387 getPreviewName = function (this: VideoInstance) {
388 const extension = '.jpg'
389 return this.uuid + extension
392 getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) {
393 const extension = '.torrent'
394 return this.uuid + '-' + videoFile.resolution + extension
397 isOwned = function (this: VideoInstance) {
398 return this.remote === false
401 createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) {
402 const imageSize = PREVIEWS_SIZE.width + 'x' + PREVIEWS_SIZE.height
404 return generateImageFromVideoFile(
405 this.getVideoFilePath(videoFile),
406 CONFIG.STORAGE.PREVIEWS_DIR,
407 this.getPreviewName(),
412 createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) {
413 const imageSize = THUMBNAILS_SIZE.width + 'x' + THUMBNAILS_SIZE.height
415 return generateImageFromVideoFile(
416 this.getVideoFilePath(videoFile),
417 CONFIG.STORAGE.THUMBNAILS_DIR,
418 this.getThumbnailName(),
423 getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance) {
424 return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
427 createTorrentAndSetInfoHash = function (this: VideoInstance, videoFile: VideoFileInstance) {
430 [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
433 CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
437 return createTorrentPromise(this.getVideoFilePath(videoFile), options)
439 const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
440 logger.info('Creating torrent %s.', filePath)
442 return writeFilePromise(filePath, torrent).then(() => torrent)
445 const parsedTorrent = parseTorrent(torrent)
447 videoFile.infoHash = parsedTorrent.infoHash
451 getEmbedPath = function (this: VideoInstance) {
452 return '/videos/embed/' + this.uuid
455 getThumbnailPath = function (this: VideoInstance) {
456 return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName())
459 getPreviewPath = function (this: VideoInstance) {
460 return join(STATIC_PATHS.PREVIEWS, this.getPreviewName())
463 toFormattedJSON = function (this: VideoInstance) {
466 if (this.VideoChannel.Author.Pod) {
467 podHost = this.VideoChannel.Author.Pod.host
469 // It means it's our video
470 podHost = CONFIG.WEBSERVER.HOST
473 // Maybe our pod is not up to date and there are new categories since our version
474 let categoryLabel = VIDEO_CATEGORIES[this.category]
475 if (!categoryLabel) categoryLabel = 'Misc'
477 // Maybe our pod is not up to date and there are new licences since our version
478 let licenceLabel = VIDEO_LICENCES[this.licence]
479 if (!licenceLabel) licenceLabel = 'Unknown'
481 // Language is an optional attribute
482 let languageLabel = VIDEO_LANGUAGES[this.language]
483 if (!languageLabel) languageLabel = 'Unknown'
489 category: this.category,
491 licence: this.licence,
493 language: this.language,
496 description: this.getTruncatedDescription(),
498 isLocal: this.isOwned(),
499 author: this.VideoChannel.Author.name,
500 duration: this.duration,
503 dislikes: this.dislikes,
504 tags: map<TagInstance, string>(this.Tags, 'name'),
505 thumbnailPath: this.getThumbnailPath(),
506 previewPath: this.getPreviewPath(),
507 embedPath: this.getEmbedPath(),
508 createdAt: this.createdAt,
509 updatedAt: this.updatedAt
515 toFormattedDetailsJSON = function (this: VideoInstance) {
516 const formattedJson = this.toFormattedJSON()
518 // Maybe our pod is not up to date and there are new privacy settings since our version
519 let privacyLabel = VIDEO_PRIVACIES[this.privacy]
520 if (!privacyLabel) privacyLabel = 'Unknown'
522 const detailsJson = {
524 privacy: this.privacy,
525 descriptionPath: this.getDescriptionPath(),
526 channel: this.VideoChannel.toFormattedJSON(),
530 // Format and sort video files
531 const { baseUrlHttp, baseUrlWs } = getBaseUrls(this)
532 detailsJson.files = this.VideoFiles
534 let resolutionLabel = videoFile.resolution + 'p'
536 const videoFileJson = {
537 resolution: videoFile.resolution,
539 magnetUri: generateMagnetUri(this, videoFile, baseUrlHttp, baseUrlWs),
540 size: videoFile.size,
541 torrentUrl: getTorrentUrl(this, videoFile, baseUrlHttp),
542 fileUrl: getVideoFileUrl(this, videoFile, baseUrlHttp)
548 if (a.resolution < b.resolution) return 1
549 if (a.resolution === b.resolution) return 0
553 return Object.assign(formattedJson, detailsJson)
556 toAddRemoteJSON = function (this: VideoInstance) {
557 // Get thumbnail data to send to the other pod
558 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
560 return readFileBufferPromise(thumbnailPath).then(thumbnailData => {
561 const remoteVideo = {
564 category: this.category,
565 licence: this.licence,
566 language: this.language,
568 truncatedDescription: this.getTruncatedDescription(),
569 channelUUID: this.VideoChannel.uuid,
570 duration: this.duration,
571 thumbnailData: thumbnailData.toString('binary'),
572 tags: map<TagInstance, string>(this.Tags, 'name'),
573 createdAt: this.createdAt,
574 updatedAt: this.updatedAt,
577 dislikes: this.dislikes,
578 privacy: this.privacy,
582 this.VideoFiles.forEach(videoFile => {
583 remoteVideo.files.push({
584 infoHash: videoFile.infoHash,
585 resolution: videoFile.resolution,
586 extname: videoFile.extname,
595 toUpdateRemoteJSON = function (this: VideoInstance) {
599 category: this.category,
600 licence: this.licence,
601 language: this.language,
603 truncatedDescription: this.getTruncatedDescription(),
604 duration: this.duration,
605 tags: map<TagInstance, string>(this.Tags, 'name'),
606 createdAt: this.createdAt,
607 updatedAt: this.updatedAt,
610 dislikes: this.dislikes,
611 privacy: this.privacy,
615 this.VideoFiles.forEach(videoFile => {
617 infoHash: videoFile.infoHash,
618 resolution: videoFile.resolution,
619 extname: videoFile.extname,
627 getTruncatedDescription = function (this: VideoInstance) {
629 length: CONSTRAINTS_FIELDS.VIDEOS.TRUNCATED_DESCRIPTION.max
632 return truncate(this.description, options)
635 optimizeOriginalVideofile = function (this: VideoInstance) {
636 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
637 const newExtname = '.mp4'
638 const inputVideoFile = this.getOriginalFile()
639 const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
640 const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
642 const transcodeOptions = {
643 inputPath: videoInputPath,
644 outputPath: videoOutputPath
647 return transcode(transcodeOptions)
649 return unlinkPromise(videoInputPath)
652 // Important to do this before getVideoFilename() to take in account the new file extension
653 inputVideoFile.set('extname', newExtname)
655 return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile))
658 return statPromise(this.getVideoFilePath(inputVideoFile))
661 return inputVideoFile.set('size', stats.size)
664 return this.createTorrentAndSetInfoHash(inputVideoFile)
667 return inputVideoFile.save()
673 // Auto destruction...
674 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
680 transcodeOriginalVideofile = function (this: VideoInstance, resolution: VideoResolution) {
681 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
682 const extname = '.mp4'
684 // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
685 const videoInputPath = join(videosDirectory, this.getVideoFilename(this.getOriginalFile()))
687 const newVideoFile = (Video['sequelize'].models.VideoFile as VideoFileModel).build({
693 const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile))
695 const transcodeOptions = {
696 inputPath: videoInputPath,
697 outputPath: videoOutputPath,
700 return transcode(transcodeOptions)
702 return statPromise(videoOutputPath)
705 newVideoFile.set('size', stats.size)
710 return this.createTorrentAndSetInfoHash(newVideoFile)
713 return newVideoFile.save()
716 return this.VideoFiles.push(newVideoFile)
718 .then(() => undefined)
721 getOriginalFileHeight = function (this: VideoInstance) {
722 const originalFilePath = this.getVideoFilePath(this.getOriginalFile())
724 return getVideoFileHeight(originalFilePath)
727 getDescriptionPath = function (this: VideoInstance) {
728 return `/api/${API_VERSION}/videos/${this.uuid}/description`
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 listUserVideosForApi = function (userId: number, start: number, count: number, sort: string) {
776 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
779 model: Video['sequelize'].models.VideoChannel,
783 model: Video['sequelize'].models.Author,
791 Video['sequelize'].models.Tag
795 return Video.findAndCountAll(query).then(({ rows, count }) => {
803 listForApi = function (start: number, count: number, sort: string) {
808 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
811 model: Video['sequelize'].models.VideoChannel,
814 model: Video['sequelize'].models.Author,
817 model: Video['sequelize'].models.Pod,
824 Video['sequelize'].models.Tag
826 where: createBaseVideosWhere()
829 return Video.findAndCountAll(query).then(({ rows, count }) => {
837 loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Transaction) {
838 const query: Sequelize.FindOptions<VideoAttributes> = {
844 model: Video['sequelize'].models.VideoFile
847 model: Video['sequelize'].models.VideoChannel,
850 model: Video['sequelize'].models.Author,
853 model: Video['sequelize'].models.Pod,
866 if (t !== undefined) query.transaction = t
868 return Video.findOne(query)
871 listOwnedAndPopulateAuthorAndTags = function () {
877 Video['sequelize'].models.VideoFile,
879 model: Video['sequelize'].models.VideoChannel,
880 include: [ Video['sequelize'].models.Author ]
882 Video['sequelize'].models.Tag
886 return Video.findAll(query)
889 listOwnedByAuthor = function (author: string) {
896 model: Video['sequelize'].models.VideoFile
899 model: Video['sequelize'].models.VideoChannel,
902 model: Video['sequelize'].models.Author,
912 return Video.findAll(query)
915 load = function (id: number) {
916 return Video.findById(id)
919 loadByUUID = function (uuid: string, t?: Sequelize.Transaction) {
920 const query: Sequelize.FindOptions<VideoAttributes> = {
924 include: [ Video['sequelize'].models.VideoFile ]
927 if (t !== undefined) query.transaction = t
929 return Video.findOne(query)
932 loadLocalVideoByUUID = function (uuid: string, t?: Sequelize.Transaction) {
933 const query: Sequelize.FindOptions<VideoAttributes> = {
938 include: [ Video['sequelize'].models.VideoFile ]
941 if (t !== undefined) query.transaction = t
943 return Video.findOne(query)
946 loadAndPopulateAuthor = function (id: number) {
949 Video['sequelize'].models.VideoFile,
951 model: Video['sequelize'].models.VideoChannel,
952 include: [ Video['sequelize'].models.Author ]
957 return Video.findById(id, options)
960 loadAndPopulateAuthorAndPodAndTags = function (id: number) {
964 model: Video['sequelize'].models.VideoChannel,
967 model: Video['sequelize'].models.Author,
968 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
972 Video['sequelize'].models.Tag,
973 Video['sequelize'].models.VideoFile
977 return Video.findById(id, options)
980 loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) {
987 model: Video['sequelize'].models.VideoChannel,
990 model: Video['sequelize'].models.Author,
991 include: [ { model: Video['sequelize'].models.Pod, required: false } ]
995 Video['sequelize'].models.Tag,
996 Video['sequelize'].models.VideoFile
1000 return Video.findOne(options)
1003 searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, start: number, count: number, sort: string) {
1004 const podInclude: Sequelize.IncludeOptions = {
1005 model: Video['sequelize'].models.Pod,
1009 const authorInclude: Sequelize.IncludeOptions = {
1010 model: Video['sequelize'].models.Author,
1011 include: [ podInclude ]
1014 const videoChannelInclude: Sequelize.IncludeOptions = {
1015 model: Video['sequelize'].models.VideoChannel,
1016 include: [ authorInclude ],
1020 const tagInclude: Sequelize.IncludeOptions = {
1021 model: Video['sequelize'].models.Tag
1024 const query: Sequelize.FindOptions<VideoAttributes> = {
1026 where: createBaseVideosWhere(),
1029 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ]
1032 if (field === 'tags') {
1033 const escapedValue = Video['sequelize'].escape('%' + value + '%')
1034 query.where['id'][Sequelize.Op.in] = Video['sequelize'].literal(
1035 `(SELECT "VideoTags"."videoId"
1037 INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId"
1038 WHERE name ILIKE ${escapedValue}
1041 } else if (field === 'host') {
1042 // FIXME: Include our pod? (not stored in the database)
1043 podInclude.where = {
1045 [Sequelize.Op.iLike]: '%' + value + '%'
1048 podInclude.required = true
1049 } else if (field === 'author') {
1050 authorInclude.where = {
1052 [Sequelize.Op.iLike]: '%' + value + '%'
1056 query.where[field] = {
1057 [Sequelize.Op.iLike]: '%' + value + '%'
1062 videoChannelInclude, tagInclude
1065 return Video.findAndCountAll(query).then(({ rows, count }) => {
1073 // ---------------------------------------------------------------------------
1075 function createBaseVideosWhere () {
1078 [Sequelize.Op.notIn]: Video['sequelize'].literal(
1079 '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")'
1082 privacy: VideoPrivacy.PUBLIC
1086 function getBaseUrls (video: VideoInstance) {
1090 if (video.isOwned()) {
1091 baseUrlHttp = CONFIG.WEBSERVER.URL
1092 baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
1094 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + video.VideoChannel.Author.Pod.host
1095 baseUrlWs = REMOTE_SCHEME.WS + '://' + video.VideoChannel.Author.Pod.host
1098 return { baseUrlHttp, baseUrlWs }
1101 function getTorrentUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) {
1102 return baseUrlHttp + STATIC_PATHS.TORRENTS + video.getTorrentFileName(videoFile)
1105 function getVideoFileUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) {
1106 return baseUrlHttp + STATIC_PATHS.WEBSEED + video.getVideoFilename(videoFile)
1109 function generateMagnetUri (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string, baseUrlWs: string) {
1110 const xs = getTorrentUrl(video, videoFile, baseUrlHttp)
1111 const announce = [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
1112 const urlList = [ getVideoFileUrl(video, videoFile, baseUrlHttp) ]
1114 const magnetHash = {
1118 infoHash: videoFile.infoHash,
1122 return magnetUtil.encode(magnetHash)