2017-10-31 10:31:24 -05:00
|
|
|
import { map, maxBy, truncate } from 'lodash'
|
2017-11-10 10:27:49 -06:00
|
|
|
import * as magnetUtil from 'magnet-uri'
|
2017-06-05 14:53:49 -05:00
|
|
|
import * as parseTorrent from 'parse-torrent'
|
2017-05-15 15:22:03 -05:00
|
|
|
import { join } from 'path'
|
2017-11-10 10:27:49 -06:00
|
|
|
import * as safeBuffer from 'safe-buffer'
|
2017-05-22 13:58:25 -05:00
|
|
|
import * as Sequelize from 'sequelize'
|
2017-11-10 10:27:49 -06:00
|
|
|
import { VideoPrivacy, VideoResolution } from '../../../shared'
|
|
|
|
import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object'
|
2017-05-15 15:22:03 -05:00
|
|
|
import {
|
2017-11-10 10:27:49 -06:00
|
|
|
createTorrentPromise,
|
|
|
|
generateImageFromVideoFile,
|
|
|
|
getActivityPubUrl,
|
|
|
|
getVideoFileHeight,
|
2017-05-15 15:22:03 -05:00
|
|
|
isVideoCategoryValid,
|
|
|
|
isVideoDescriptionValid,
|
2017-07-05 06:26:25 -05:00
|
|
|
isVideoDurationValid,
|
2017-11-10 10:27:49 -06:00
|
|
|
isVideoLanguageValid,
|
|
|
|
isVideoLicenceValid,
|
|
|
|
isVideoNameValid,
|
|
|
|
isVideoNSFWValid,
|
2017-10-31 05:52:52 -05:00
|
|
|
isVideoPrivacyValid,
|
2017-11-10 10:27:49 -06:00
|
|
|
logger,
|
2017-07-05 06:26:25 -05:00
|
|
|
renamePromise,
|
2017-10-09 04:06:13 -05:00
|
|
|
statPromise,
|
|
|
|
transcode,
|
2017-11-10 10:27:49 -06:00
|
|
|
unlinkPromise,
|
|
|
|
writeFilePromise
|
2017-06-16 02:45:46 -05:00
|
|
|
} from '../../helpers'
|
2017-05-15 15:22:03 -05:00
|
|
|
import {
|
2017-11-10 10:27:49 -06:00
|
|
|
API_VERSION,
|
2017-05-15 15:22:03 -05:00
|
|
|
CONFIG,
|
2017-11-10 10:27:49 -06:00
|
|
|
CONSTRAINTS_FIELDS,
|
|
|
|
PREVIEWS_SIZE,
|
2017-05-15 15:22:03 -05:00
|
|
|
REMOTE_SCHEME,
|
|
|
|
STATIC_PATHS,
|
2017-11-10 10:27:49 -06:00
|
|
|
THUMBNAILS_SIZE,
|
2017-05-15 15:22:03 -05:00
|
|
|
VIDEO_CATEGORIES,
|
|
|
|
VIDEO_LANGUAGES,
|
2017-11-10 10:27:49 -06:00
|
|
|
VIDEO_LICENCES,
|
2017-10-31 05:52:52 -05:00
|
|
|
VIDEO_PRIVACIES
|
2017-06-16 02:45:46 -05:00
|
|
|
} from '../../initializers'
|
2016-06-24 10:42:51 -05:00
|
|
|
|
2017-06-16 02:45:46 -05:00
|
|
|
import { addMethodsToModel, getSort } from '../utils'
|
2017-05-22 13:58:25 -05:00
|
|
|
|
2017-11-10 10:27:49 -06:00
|
|
|
import { TagInstance } from './tag-interface'
|
|
|
|
import { VideoFileInstance, VideoFileModel } from './video-file-interface'
|
|
|
|
import { VideoAttributes, VideoInstance, VideoMethods } from './video-interface'
|
|
|
|
|
|
|
|
const Buffer = safeBuffer.Buffer
|
2017-05-22 13:58:25 -05:00
|
|
|
|
|
|
|
let Video: Sequelize.Model<VideoInstance, VideoAttributes>
|
2017-10-02 05:20:26 -05:00
|
|
|
let getOriginalFile: VideoMethods.GetOriginalFile
|
2017-05-22 13:58:25 -05:00
|
|
|
let getVideoFilename: VideoMethods.GetVideoFilename
|
|
|
|
let getThumbnailName: VideoMethods.GetThumbnailName
|
2017-10-16 03:05:49 -05:00
|
|
|
let getThumbnailPath: VideoMethods.GetThumbnailPath
|
2017-05-22 13:58:25 -05:00
|
|
|
let getPreviewName: VideoMethods.GetPreviewName
|
2017-10-16 03:05:49 -05:00
|
|
|
let getPreviewPath: VideoMethods.GetPreviewPath
|
2017-08-25 04:36:23 -05:00
|
|
|
let getTorrentFileName: VideoMethods.GetTorrentFileName
|
2017-05-22 13:58:25 -05:00
|
|
|
let isOwned: VideoMethods.IsOwned
|
2017-08-25 04:45:31 -05:00
|
|
|
let toFormattedJSON: VideoMethods.ToFormattedJSON
|
2017-10-24 12:41:09 -05:00
|
|
|
let toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON
|
2017-11-09 10:51:58 -06:00
|
|
|
let toActivityPubObject: VideoMethods.ToActivityPubObject
|
2017-10-02 05:20:26 -05:00
|
|
|
let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
|
|
|
|
let transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
|
2017-08-25 04:36:23 -05:00
|
|
|
let createPreview: VideoMethods.CreatePreview
|
|
|
|
let createThumbnail: VideoMethods.CreateThumbnail
|
|
|
|
let getVideoFilePath: VideoMethods.GetVideoFilePath
|
|
|
|
let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
|
2017-10-02 05:20:26 -05:00
|
|
|
let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
|
2017-10-16 03:05:49 -05:00
|
|
|
let getEmbedPath: VideoMethods.GetEmbedPath
|
2017-10-30 04:16:27 -05:00
|
|
|
let getDescriptionPath: VideoMethods.GetDescriptionPath
|
|
|
|
let getTruncatedDescription: VideoMethods.GetTruncatedDescription
|
2017-11-09 10:51:58 -06:00
|
|
|
let getCategoryLabel: VideoMethods.GetCategoryLabel
|
|
|
|
let getLicenceLabel: VideoMethods.GetLicenceLabel
|
|
|
|
let getLanguageLabel: VideoMethods.GetLanguageLabel
|
2017-05-22 13:58:25 -05:00
|
|
|
|
|
|
|
let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
|
|
|
|
let list: VideoMethods.List
|
|
|
|
let listForApi: VideoMethods.ListForApi
|
2017-10-31 05:52:52 -05:00
|
|
|
let listUserVideosForApi: VideoMethods.ListUserVideosForApi
|
2017-07-11 09:01:56 -05:00
|
|
|
let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
|
2017-11-09 10:51:58 -06:00
|
|
|
let listOwnedAndPopulateAccountAndTags: VideoMethods.ListOwnedAndPopulateAccountAndTags
|
|
|
|
let listOwnedByAccount: VideoMethods.ListOwnedByAccount
|
2017-05-22 13:58:25 -05:00
|
|
|
let load: VideoMethods.Load
|
2017-07-11 09:01:56 -05:00
|
|
|
let loadByUUID: VideoMethods.LoadByUUID
|
2017-11-10 07:34:45 -06:00
|
|
|
let loadByUUIDOrURL: VideoMethods.LoadByUUIDOrURL
|
2017-10-26 04:26:35 -05:00
|
|
|
let loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID
|
2017-11-09 10:51:58 -06:00
|
|
|
let loadAndPopulateAccount: VideoMethods.LoadAndPopulateAccount
|
|
|
|
let loadAndPopulateAccountAndPodAndTags: VideoMethods.LoadAndPopulateAccountAndPodAndTags
|
|
|
|
let loadByUUIDAndPopulateAccountAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAccountAndPodAndTags
|
|
|
|
let searchAndPopulateAccountAndPodAndTags: VideoMethods.SearchAndPopulateAccountAndPodAndTags
|
2017-08-25 04:36:23 -05:00
|
|
|
let removeThumbnail: VideoMethods.RemoveThumbnail
|
|
|
|
let removePreview: VideoMethods.RemovePreview
|
|
|
|
let removeFile: VideoMethods.RemoveFile
|
|
|
|
let removeTorrent: VideoMethods.RemoveTorrent
|
2017-05-22 13:58:25 -05:00
|
|
|
|
2017-06-11 10:35:32 -05:00
|
|
|
export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
|
|
|
|
Video = sequelize.define<VideoInstance, VideoAttributes>('Video',
|
2016-12-11 14:50:51 -06:00
|
|
|
{
|
2017-07-11 09:01:56 -05:00
|
|
|
uuid: {
|
2016-12-11 14:50:51 -06:00
|
|
|
type: DataTypes.UUID,
|
|
|
|
defaultValue: DataTypes.UUIDV4,
|
2017-07-11 09:01:56 -05:00
|
|
|
allowNull: false,
|
2016-12-28 08:49:23 -06:00
|
|
|
validate: {
|
|
|
|
isUUID: 4
|
|
|
|
}
|
2016-06-24 10:42:51 -05:00
|
|
|
},
|
2016-12-11 14:50:51 -06:00
|
|
|
name: {
|
2016-12-28 08:49:23 -06:00
|
|
|
type: DataTypes.STRING,
|
|
|
|
allowNull: false,
|
|
|
|
validate: {
|
2017-07-11 10:04:57 -05:00
|
|
|
nameValid: value => {
|
2017-05-15 15:22:03 -05:00
|
|
|
const res = isVideoNameValid(value)
|
2016-12-28 08:49:23 -06:00
|
|
|
if (res === false) throw new Error('Video name is not valid.')
|
|
|
|
}
|
|
|
|
}
|
2016-11-11 04:52:24 -06:00
|
|
|
},
|
2017-03-22 15:15:55 -05:00
|
|
|
category: {
|
|
|
|
type: DataTypes.INTEGER,
|
|
|
|
allowNull: false,
|
|
|
|
validate: {
|
2017-07-11 10:04:57 -05:00
|
|
|
categoryValid: value => {
|
2017-05-15 15:22:03 -05:00
|
|
|
const res = isVideoCategoryValid(value)
|
2017-03-22 15:15:55 -05:00
|
|
|
if (res === false) throw new Error('Video category is not valid.')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2017-03-27 13:53:11 -05:00
|
|
|
licence: {
|
|
|
|
type: DataTypes.INTEGER,
|
|
|
|
allowNull: false,
|
2017-04-07 05:13:37 -05:00
|
|
|
defaultValue: null,
|
2017-03-27 13:53:11 -05:00
|
|
|
validate: {
|
2017-07-11 10:04:57 -05:00
|
|
|
licenceValid: value => {
|
2017-05-15 15:22:03 -05:00
|
|
|
const res = isVideoLicenceValid(value)
|
2017-03-27 13:53:11 -05:00
|
|
|
if (res === false) throw new Error('Video licence is not valid.')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2017-04-07 05:13:37 -05:00
|
|
|
language: {
|
|
|
|
type: DataTypes.INTEGER,
|
|
|
|
allowNull: true,
|
|
|
|
validate: {
|
2017-07-11 10:04:57 -05:00
|
|
|
languageValid: value => {
|
2017-05-15 15:22:03 -05:00
|
|
|
const res = isVideoLanguageValid(value)
|
2017-04-07 05:13:37 -05:00
|
|
|
if (res === false) throw new Error('Video language is not valid.')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2017-10-31 05:52:52 -05:00
|
|
|
privacy: {
|
|
|
|
type: DataTypes.INTEGER,
|
|
|
|
allowNull: false,
|
|
|
|
validate: {
|
|
|
|
privacyValid: value => {
|
|
|
|
const res = isVideoPrivacyValid(value)
|
|
|
|
if (res === false) throw new Error('Video privacy is not valid.')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2017-03-28 14:19:46 -05:00
|
|
|
nsfw: {
|
|
|
|
type: DataTypes.BOOLEAN,
|
|
|
|
allowNull: false,
|
|
|
|
validate: {
|
2017-07-11 10:04:57 -05:00
|
|
|
nsfwValid: value => {
|
2017-05-15 15:22:03 -05:00
|
|
|
const res = isVideoNSFWValid(value)
|
2017-03-28 14:19:46 -05:00
|
|
|
if (res === false) throw new Error('Video nsfw attribute is not valid.')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2016-12-11 14:50:51 -06:00
|
|
|
description: {
|
2017-10-30 04:16:27 -05:00
|
|
|
type: DataTypes.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max),
|
2016-12-28 08:49:23 -06:00
|
|
|
allowNull: false,
|
|
|
|
validate: {
|
2017-07-11 10:04:57 -05:00
|
|
|
descriptionValid: value => {
|
2017-05-15 15:22:03 -05:00
|
|
|
const res = isVideoDescriptionValid(value)
|
2016-12-28 08:49:23 -06:00
|
|
|
if (res === false) throw new Error('Video description is not valid.')
|
|
|
|
}
|
|
|
|
}
|
2016-12-11 14:50:51 -06:00
|
|
|
},
|
|
|
|
duration: {
|
2016-12-28 08:49:23 -06:00
|
|
|
type: DataTypes.INTEGER,
|
|
|
|
allowNull: false,
|
|
|
|
validate: {
|
2017-07-11 10:04:57 -05:00
|
|
|
durationValid: value => {
|
2017-05-15 15:22:03 -05:00
|
|
|
const res = isVideoDurationValid(value)
|
2016-12-28 08:49:23 -06:00
|
|
|
if (res === false) throw new Error('Video duration is not valid.')
|
|
|
|
}
|
|
|
|
}
|
2017-02-21 14:35:59 -06:00
|
|
|
},
|
|
|
|
views: {
|
|
|
|
type: DataTypes.INTEGER,
|
|
|
|
allowNull: false,
|
|
|
|
defaultValue: 0,
|
|
|
|
validate: {
|
|
|
|
min: 0,
|
|
|
|
isInt: true
|
|
|
|
}
|
2017-03-08 14:35:43 -06:00
|
|
|
},
|
|
|
|
likes: {
|
|
|
|
type: DataTypes.INTEGER,
|
|
|
|
allowNull: false,
|
|
|
|
defaultValue: 0,
|
|
|
|
validate: {
|
|
|
|
min: 0,
|
|
|
|
isInt: true
|
|
|
|
}
|
|
|
|
},
|
|
|
|
dislikes: {
|
|
|
|
type: DataTypes.INTEGER,
|
|
|
|
allowNull: false,
|
|
|
|
defaultValue: 0,
|
|
|
|
validate: {
|
|
|
|
min: 0,
|
|
|
|
isInt: true
|
|
|
|
}
|
2017-07-11 09:01:56 -05:00
|
|
|
},
|
|
|
|
remote: {
|
|
|
|
type: DataTypes.BOOLEAN,
|
|
|
|
allowNull: false,
|
|
|
|
defaultValue: false
|
2017-11-09 10:51:58 -06:00
|
|
|
},
|
|
|
|
url: {
|
|
|
|
type: DataTypes.STRING,
|
|
|
|
allowNull: false,
|
|
|
|
validate: {
|
|
|
|
isUrl: true
|
|
|
|
}
|
2016-06-24 10:42:51 -05:00
|
|
|
}
|
2016-12-11 14:50:51 -06:00
|
|
|
},
|
|
|
|
{
|
2016-12-29 02:33:28 -06:00
|
|
|
indexes: [
|
|
|
|
{
|
|
|
|
fields: [ 'name' ]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
fields: [ 'createdAt' ]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
fields: [ 'duration' ]
|
|
|
|
},
|
2017-02-21 14:35:59 -06:00
|
|
|
{
|
|
|
|
fields: [ 'views' ]
|
2017-03-08 14:35:43 -06:00
|
|
|
},
|
|
|
|
{
|
|
|
|
fields: [ 'likes' ]
|
2017-07-11 09:01:56 -05:00
|
|
|
},
|
|
|
|
{
|
|
|
|
fields: [ 'uuid' ]
|
2017-10-24 12:41:09 -05:00
|
|
|
},
|
|
|
|
{
|
|
|
|
fields: [ 'channelId' ]
|
2017-11-09 10:51:58 -06:00
|
|
|
},
|
|
|
|
{
|
|
|
|
fields: [ 'parentId' ]
|
2016-12-29 02:33:28 -06:00
|
|
|
}
|
|
|
|
],
|
2016-12-11 14:50:51 -06:00
|
|
|
hooks: {
|
|
|
|
afterDestroy
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
2016-06-24 10:42:51 -05:00
|
|
|
|
2017-05-22 13:58:25 -05:00
|
|
|
const classMethods = [
|
|
|
|
associate,
|
|
|
|
|
|
|
|
generateThumbnailFromData,
|
|
|
|
list,
|
|
|
|
listForApi,
|
2017-10-31 05:52:52 -05:00
|
|
|
listUserVideosForApi,
|
2017-11-09 10:51:58 -06:00
|
|
|
listOwnedAndPopulateAccountAndTags,
|
|
|
|
listOwnedByAccount,
|
2017-05-22 13:58:25 -05:00
|
|
|
load,
|
2017-11-09 10:51:58 -06:00
|
|
|
loadAndPopulateAccount,
|
|
|
|
loadAndPopulateAccountAndPodAndTags,
|
2017-08-25 04:36:23 -05:00
|
|
|
loadByHostAndUUID,
|
2017-11-10 07:34:45 -06:00
|
|
|
loadByUUIDOrURL,
|
2017-08-25 04:36:23 -05:00
|
|
|
loadByUUID,
|
2017-10-26 04:26:35 -05:00
|
|
|
loadLocalVideoByUUID,
|
2017-11-09 10:51:58 -06:00
|
|
|
loadByUUIDAndPopulateAccountAndPodAndTags,
|
|
|
|
searchAndPopulateAccountAndPodAndTags
|
2017-05-22 13:58:25 -05:00
|
|
|
]
|
|
|
|
const instanceMethods = [
|
2017-08-25 04:36:23 -05:00
|
|
|
createPreview,
|
|
|
|
createThumbnail,
|
|
|
|
createTorrentAndSetInfoHash,
|
2017-05-22 13:58:25 -05:00
|
|
|
getPreviewName,
|
2017-10-16 03:05:49 -05:00
|
|
|
getPreviewPath,
|
2017-08-25 04:36:23 -05:00
|
|
|
getThumbnailName,
|
2017-10-16 03:05:49 -05:00
|
|
|
getThumbnailPath,
|
2017-08-25 04:36:23 -05:00
|
|
|
getTorrentFileName,
|
|
|
|
getVideoFilename,
|
|
|
|
getVideoFilePath,
|
2017-10-02 05:20:26 -05:00
|
|
|
getOriginalFile,
|
2017-05-22 13:58:25 -05:00
|
|
|
isOwned,
|
2017-08-25 04:36:23 -05:00
|
|
|
removeFile,
|
|
|
|
removePreview,
|
|
|
|
removeThumbnail,
|
|
|
|
removeTorrent,
|
2017-11-09 10:51:58 -06:00
|
|
|
toActivityPubObject,
|
2017-08-25 04:45:31 -05:00
|
|
|
toFormattedJSON,
|
2017-10-24 12:41:09 -05:00
|
|
|
toFormattedDetailsJSON,
|
2017-10-02 05:20:26 -05:00
|
|
|
optimizeOriginalVideofile,
|
|
|
|
transcodeOriginalVideofile,
|
2017-10-16 03:05:49 -05:00
|
|
|
getOriginalFileHeight,
|
2017-10-30 04:16:27 -05:00
|
|
|
getEmbedPath,
|
|
|
|
getTruncatedDescription,
|
2017-11-09 10:51:58 -06:00
|
|
|
getDescriptionPath,
|
|
|
|
getCategoryLabel,
|
|
|
|
getLicenceLabel,
|
|
|
|
getLanguageLabel
|
2017-05-22 13:58:25 -05:00
|
|
|
]
|
|
|
|
addMethodsToModel(Video, classMethods, instanceMethods)
|
|
|
|
|
2016-12-11 14:50:51 -06:00
|
|
|
return Video
|
|
|
|
}
|
2016-06-24 10:42:51 -05:00
|
|
|
|
|
|
|
// ------------------------------ METHODS ------------------------------
|
|
|
|
|
2016-12-11 14:50:51 -06:00
|
|
|
function associate (models) {
|
2017-10-24 12:41:09 -05:00
|
|
|
Video.belongsTo(models.VideoChannel, {
|
2016-12-11 14:50:51 -06:00
|
|
|
foreignKey: {
|
2017-10-24 12:41:09 -05:00
|
|
|
name: 'channelId',
|
2016-12-11 14:50:51 -06:00
|
|
|
allowNull: false
|
|
|
|
},
|
|
|
|
onDelete: 'cascade'
|
|
|
|
})
|
2016-12-24 09:59:17 -06:00
|
|
|
|
2017-11-09 10:51:58 -06:00
|
|
|
Video.belongsTo(models.VideoChannel, {
|
|
|
|
foreignKey: {
|
|
|
|
name: 'parentId',
|
|
|
|
allowNull: true
|
|
|
|
},
|
|
|
|
onDelete: 'cascade'
|
|
|
|
})
|
|
|
|
|
2017-05-22 13:58:25 -05:00
|
|
|
Video.belongsToMany(models.Tag, {
|
2016-12-24 09:59:17 -06:00
|
|
|
foreignKey: 'videoId',
|
|
|
|
through: models.VideoTag,
|
|
|
|
onDelete: 'cascade'
|
|
|
|
})
|
2017-01-04 13:59:23 -06:00
|
|
|
|
2017-05-22 13:58:25 -05:00
|
|
|
Video.hasMany(models.VideoAbuse, {
|
2017-01-04 13:59:23 -06:00
|
|
|
foreignKey: {
|
|
|
|
name: 'videoId',
|
|
|
|
allowNull: false
|
|
|
|
},
|
|
|
|
onDelete: 'cascade'
|
|
|
|
})
|
2017-08-25 04:36:23 -05:00
|
|
|
|
|
|
|
Video.hasMany(models.VideoFile, {
|
|
|
|
foreignKey: {
|
|
|
|
name: 'videoId',
|
|
|
|
allowNull: false
|
|
|
|
},
|
|
|
|
onDelete: 'cascade'
|
|
|
|
})
|
2016-12-11 14:50:51 -06:00
|
|
|
}
|
|
|
|
|
2017-10-26 07:05:20 -05:00
|
|
|
function afterDestroy (video: VideoInstance) {
|
2017-08-25 04:36:23 -05:00
|
|
|
const tasks = []
|
2016-11-11 08:20:03 -06:00
|
|
|
|
2017-08-25 04:36:23 -05:00
|
|
|
tasks.push(
|
|
|
|
video.removeThumbnail()
|
|
|
|
)
|
2016-11-11 08:20:03 -06:00
|
|
|
|
2017-08-25 04:36:23 -05:00
|
|
|
if (video.isOwned()) {
|
|
|
|
const removeVideoToFriendsParams = {
|
|
|
|
uuid: video.uuid
|
|
|
|
}
|
2016-11-11 08:20:03 -06:00
|
|
|
|
2017-08-25 04:36:23 -05:00
|
|
|
tasks.push(
|
2017-11-10 10:27:49 -06:00
|
|
|
video.removePreview()
|
|
|
|
// FIXME: remove video for followers
|
2017-08-25 04:36:23 -05:00
|
|
|
)
|
|
|
|
|
2017-09-12 07:17:46 -05:00
|
|
|
// Remove physical files and torrents
|
2017-08-25 04:36:23 -05:00
|
|
|
video.VideoFiles.forEach(file => {
|
2017-10-19 02:28:35 -05:00
|
|
|
tasks.push(video.removeFile(file))
|
|
|
|
tasks.push(video.removeTorrent(file))
|
2017-08-25 04:36:23 -05:00
|
|
|
})
|
2016-11-11 08:20:03 -06:00
|
|
|
}
|
|
|
|
|
2017-08-25 04:36:23 -05:00
|
|
|
return Promise.all(tasks)
|
2017-10-19 02:28:35 -05:00
|
|
|
.catch(err => {
|
2017-10-26 05:06:57 -05:00
|
|
|
logger.error('Some errors when removing files of video %s in after destroy hook.', video.uuid, err)
|
2017-10-19 02:28:35 -05:00
|
|
|
})
|
2016-11-11 06:47:50 -06:00
|
|
|
}
|
|
|
|
|
2017-10-02 05:20:26 -05:00
|
|
|
getOriginalFile = function (this: VideoInstance) {
|
|
|
|
if (Array.isArray(this.VideoFiles) === false) return undefined
|
|
|
|
|
2017-10-09 04:06:13 -05:00
|
|
|
// The original file is the file that have the higher resolution
|
|
|
|
return maxBy(this.VideoFiles, file => file.resolution)
|
2017-10-02 05:20:26 -05:00
|
|
|
}
|
|
|
|
|
2017-08-25 04:36:23 -05:00
|
|
|
getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) {
|
2017-10-09 04:06:13 -05:00
|
|
|
return this.uuid + '-' + videoFile.resolution + videoFile.extname
|
2016-11-11 08:20:03 -06:00
|
|
|
}
|
|
|
|
|
2017-06-16 02:54:59 -05:00
|
|
|
getThumbnailName = function (this: VideoInstance) {
|
2016-11-11 08:20:03 -06:00
|
|
|
// We always have a copy of the thumbnail
|
2017-07-11 09:01:56 -05:00
|
|
|
const extension = '.jpg'
|
|
|
|
return this.uuid + extension
|
2016-11-11 06:47:50 -06:00
|
|
|
}
|
|
|
|
|
2017-06-16 02:54:59 -05:00
|
|
|
getPreviewName = function (this: VideoInstance) {
|
2016-11-11 08:20:03 -06:00
|
|
|
const extension = '.jpg'
|
2017-07-11 09:01:56 -05:00
|
|
|
return this.uuid + extension
|
2016-11-11 08:20:03 -06:00
|
|
|
}
|
|
|
|
|
2017-08-25 04:36:23 -05:00
|
|
|
getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) {
|
2016-11-11 08:20:03 -06:00
|
|
|
const extension = '.torrent'
|
2017-10-09 04:06:13 -05:00
|
|
|
return this.uuid + '-' + videoFile.resolution + extension
|
2016-11-11 06:47:50 -06:00
|
|
|
}
|
|
|
|
|
2017-06-16 02:54:59 -05:00
|
|
|
isOwned = function (this: VideoInstance) {
|
2017-07-11 09:01:56 -05:00
|
|
|
return this.remote === false
|
2016-06-24 10:42:51 -05:00
|
|
|
}
|
|
|
|
|
2017-08-25 04:36:23 -05:00
|
|
|
createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) {
|
2017-10-17 03:35:27 -05:00
|
|
|
const imageSize = PREVIEWS_SIZE.width + 'x' + PREVIEWS_SIZE.height
|
|
|
|
|
2017-10-09 04:06:13 -05:00
|
|
|
return generateImageFromVideoFile(
|
|
|
|
this.getVideoFilePath(videoFile),
|
|
|
|
CONFIG.STORAGE.PREVIEWS_DIR,
|
2017-10-17 03:35:27 -05:00
|
|
|
this.getPreviewName(),
|
|
|
|
imageSize
|
2017-10-09 04:06:13 -05:00
|
|
|
)
|
2017-08-25 04:36:23 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) {
|
2017-10-16 03:05:49 -05:00
|
|
|
const imageSize = THUMBNAILS_SIZE.width + 'x' + THUMBNAILS_SIZE.height
|
|
|
|
|
2017-10-09 04:06:13 -05:00
|
|
|
return generateImageFromVideoFile(
|
|
|
|
this.getVideoFilePath(videoFile),
|
|
|
|
CONFIG.STORAGE.THUMBNAILS_DIR,
|
|
|
|
this.getThumbnailName(),
|
2017-10-16 03:05:49 -05:00
|
|
|
imageSize
|
2017-10-09 04:06:13 -05:00
|
|
|
)
|
2017-08-25 04:36:23 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance) {
|
|
|
|
return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
|
|
|
|
}
|
|
|
|
|
2017-11-09 10:51:58 -06:00
|
|
|
createTorrentAndSetInfoHash = async function (this: VideoInstance, videoFile: VideoFileInstance) {
|
2017-08-25 04:36:23 -05:00
|
|
|
const options = {
|
|
|
|
announceList: [
|
|
|
|
[ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
|
|
|
|
],
|
|
|
|
urlList: [
|
|
|
|
CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2017-11-09 10:51:58 -06:00
|
|
|
const torrent = await createTorrentPromise(this.getVideoFilePath(videoFile), options)
|
2017-09-07 08:27:35 -05:00
|
|
|
|
2017-11-09 10:51:58 -06:00
|
|
|
const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
|
|
|
|
logger.info('Creating torrent %s.', filePath)
|
2017-08-25 04:36:23 -05:00
|
|
|
|
2017-11-09 10:51:58 -06:00
|
|
|
await writeFilePromise(filePath, torrent)
|
|
|
|
|
|
|
|
const parsedTorrent = parseTorrent(torrent)
|
|
|
|
videoFile.infoHash = parsedTorrent.infoHash
|
2017-08-25 04:36:23 -05:00
|
|
|
}
|
|
|
|
|
2017-10-16 03:05:49 -05:00
|
|
|
getEmbedPath = function (this: VideoInstance) {
|
|
|
|
return '/videos/embed/' + this.uuid
|
|
|
|
}
|
|
|
|
|
|
|
|
getThumbnailPath = function (this: VideoInstance) {
|
|
|
|
return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName())
|
|
|
|
}
|
|
|
|
|
|
|
|
getPreviewPath = function (this: VideoInstance) {
|
|
|
|
return join(STATIC_PATHS.PREVIEWS, this.getPreviewName())
|
|
|
|
}
|
|
|
|
|
2017-08-25 04:45:31 -05:00
|
|
|
toFormattedJSON = function (this: VideoInstance) {
|
2016-12-11 14:50:51 -06:00
|
|
|
let podHost
|
|
|
|
|
2017-11-09 10:51:58 -06:00
|
|
|
if (this.VideoChannel.Account.Pod) {
|
|
|
|
podHost = this.VideoChannel.Account.Pod.host
|
2016-12-11 14:50:51 -06:00
|
|
|
} else {
|
|
|
|
// It means it's our video
|
2017-05-15 15:22:03 -05:00
|
|
|
podHost = CONFIG.WEBSERVER.HOST
|
2016-12-11 14:50:51 -06:00
|
|
|
}
|
|
|
|
|
2016-06-24 10:42:51 -05:00
|
|
|
const json = {
|
2016-12-11 14:50:51 -06:00
|
|
|
id: this.id,
|
2017-07-11 09:01:56 -05:00
|
|
|
uuid: this.uuid,
|
2016-06-24 10:42:51 -05:00
|
|
|
name: this.name,
|
2017-03-22 15:15:55 -05:00
|
|
|
category: this.category,
|
2017-11-09 10:51:58 -06:00
|
|
|
categoryLabel: this.getCategoryLabel(),
|
2017-03-27 13:53:11 -05:00
|
|
|
licence: this.licence,
|
2017-11-09 10:51:58 -06:00
|
|
|
licenceLabel: this.getLicenceLabel(),
|
2017-04-07 05:13:37 -05:00
|
|
|
language: this.language,
|
2017-11-09 10:51:58 -06:00
|
|
|
languageLabel: this.getLanguageLabel(),
|
2017-03-28 14:19:46 -05:00
|
|
|
nsfw: this.nsfw,
|
2017-10-30 04:16:27 -05:00
|
|
|
description: this.getTruncatedDescription(),
|
2016-12-11 14:50:51 -06:00
|
|
|
podHost,
|
2016-06-24 10:42:51 -05:00
|
|
|
isLocal: this.isOwned(),
|
2017-11-09 10:51:58 -06:00
|
|
|
account: this.VideoChannel.Account.name,
|
2017-10-24 12:41:09 -05:00
|
|
|
duration: this.duration,
|
|
|
|
views: this.views,
|
|
|
|
likes: this.likes,
|
|
|
|
dislikes: this.dislikes,
|
|
|
|
tags: map<TagInstance, string>(this.Tags, 'name'),
|
|
|
|
thumbnailPath: this.getThumbnailPath(),
|
|
|
|
previewPath: this.getPreviewPath(),
|
|
|
|
embedPath: this.getEmbedPath(),
|
|
|
|
createdAt: this.createdAt,
|
|
|
|
updatedAt: this.updatedAt
|
|
|
|
}
|
|
|
|
|
|
|
|
return json
|
|
|
|
}
|
|
|
|
|
|
|
|
toFormattedDetailsJSON = function (this: VideoInstance) {
|
2017-10-30 04:16:27 -05:00
|
|
|
const formattedJson = this.toFormattedJSON()
|
2017-10-24 12:41:09 -05:00
|
|
|
|
2017-10-31 05:52:52 -05:00
|
|
|
// Maybe our pod is not up to date and there are new privacy settings since our version
|
|
|
|
let privacyLabel = VIDEO_PRIVACIES[this.privacy]
|
|
|
|
if (!privacyLabel) privacyLabel = 'Unknown'
|
|
|
|
|
2017-10-30 04:16:27 -05:00
|
|
|
const detailsJson = {
|
2017-10-31 05:52:52 -05:00
|
|
|
privacyLabel,
|
|
|
|
privacy: this.privacy,
|
2017-10-30 04:16:27 -05:00
|
|
|
descriptionPath: this.getDescriptionPath(),
|
2017-10-24 12:41:09 -05:00
|
|
|
channel: this.VideoChannel.toFormattedJSON(),
|
2017-08-25 04:36:23 -05:00
|
|
|
files: []
|
2016-06-24 10:42:51 -05:00
|
|
|
}
|
|
|
|
|
2017-10-06 03:40:09 -05:00
|
|
|
// Format and sort video files
|
2017-10-19 07:58:28 -05:00
|
|
|
const { baseUrlHttp, baseUrlWs } = getBaseUrls(this)
|
2017-10-30 04:16:27 -05:00
|
|
|
detailsJson.files = this.VideoFiles
|
2017-10-06 03:40:09 -05:00
|
|
|
.map(videoFile => {
|
2017-10-09 04:06:13 -05:00
|
|
|
let resolutionLabel = videoFile.resolution + 'p'
|
2017-10-06 03:40:09 -05:00
|
|
|
|
|
|
|
const videoFileJson = {
|
|
|
|
resolution: videoFile.resolution,
|
|
|
|
resolutionLabel,
|
2017-10-19 07:58:28 -05:00
|
|
|
magnetUri: generateMagnetUri(this, videoFile, baseUrlHttp, baseUrlWs),
|
|
|
|
size: videoFile.size,
|
|
|
|
torrentUrl: getTorrentUrl(this, videoFile, baseUrlHttp),
|
|
|
|
fileUrl: getVideoFileUrl(this, videoFile, baseUrlHttp)
|
2017-10-06 03:40:09 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
return videoFileJson
|
|
|
|
})
|
|
|
|
.sort((a, b) => {
|
|
|
|
if (a.resolution < b.resolution) return 1
|
|
|
|
if (a.resolution === b.resolution) return 0
|
|
|
|
return -1
|
|
|
|
})
|
2017-08-25 04:36:23 -05:00
|
|
|
|
2017-10-30 04:16:27 -05:00
|
|
|
return Object.assign(formattedJson, detailsJson)
|
2016-06-24 10:42:51 -05:00
|
|
|
}
|
|
|
|
|
2017-11-09 10:51:58 -06:00
|
|
|
toActivityPubObject = function (this: VideoInstance) {
|
|
|
|
const { baseUrlHttp, baseUrlWs } = getBaseUrls(this)
|
2016-06-24 10:42:51 -05:00
|
|
|
|
2017-11-09 10:51:58 -06:00
|
|
|
const tag = this.Tags.map(t => ({
|
2017-11-10 10:27:49 -06:00
|
|
|
type: 'Hashtag' as 'Hashtag',
|
2017-11-09 10:51:58 -06:00
|
|
|
name: t.name
|
|
|
|
}))
|
|
|
|
|
|
|
|
const url = []
|
|
|
|
for (const file of this.VideoFiles) {
|
|
|
|
url.push({
|
|
|
|
type: 'Link',
|
|
|
|
mimeType: 'video/' + file.extname,
|
|
|
|
url: getVideoFileUrl(this, file, baseUrlHttp),
|
|
|
|
width: file.resolution,
|
|
|
|
size: file.size
|
|
|
|
})
|
2016-06-24 10:42:51 -05:00
|
|
|
|
2017-11-09 10:51:58 -06:00
|
|
|
url.push({
|
|
|
|
type: 'Link',
|
|
|
|
mimeType: 'application/x-bittorrent',
|
|
|
|
url: getTorrentUrl(this, file, baseUrlHttp),
|
|
|
|
width: file.resolution
|
2017-08-25 04:36:23 -05:00
|
|
|
})
|
|
|
|
|
2017-11-09 10:51:58 -06:00
|
|
|
url.push({
|
|
|
|
type: 'Link',
|
|
|
|
mimeType: 'application/x-bittorrent;x-scheme-handler/magnet',
|
|
|
|
url: generateMagnetUri(this, file, baseUrlHttp, baseUrlWs),
|
|
|
|
width: file.resolution
|
|
|
|
})
|
|
|
|
}
|
2016-06-24 10:42:51 -05:00
|
|
|
|
2017-11-09 10:51:58 -06:00
|
|
|
const videoObject: VideoTorrentObject = {
|
2017-11-10 10:27:49 -06:00
|
|
|
type: 'Video' as 'Video',
|
2017-11-10 07:34:45 -06:00
|
|
|
id: getActivityPubUrl('video', this.uuid),
|
2016-12-29 12:07:05 -06:00
|
|
|
name: this.name,
|
2017-11-09 10:51:58 -06:00
|
|
|
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
|
|
|
|
duration: 'PT' + this.duration + 'S',
|
|
|
|
uuid: this.uuid,
|
|
|
|
tag,
|
|
|
|
category: {
|
2017-11-10 10:27:49 -06:00
|
|
|
identifier: this.category + '',
|
|
|
|
name: this.getCategoryLabel()
|
2017-11-09 10:51:58 -06:00
|
|
|
},
|
|
|
|
licence: {
|
2017-11-10 10:27:49 -06:00
|
|
|
identifier: this.licence + '',
|
2017-11-09 10:51:58 -06:00
|
|
|
name: this.getLicenceLabel()
|
|
|
|
},
|
|
|
|
language: {
|
2017-11-10 10:27:49 -06:00
|
|
|
identifier: this.language + '',
|
2017-11-09 10:51:58 -06:00
|
|
|
name: this.getLanguageLabel()
|
|
|
|
},
|
2017-03-08 14:35:43 -06:00
|
|
|
views: this.views,
|
2017-11-09 10:51:58 -06:00
|
|
|
nsfw: this.nsfw,
|
|
|
|
published: this.createdAt,
|
|
|
|
updated: this.updatedAt,
|
|
|
|
mediaType: 'text/markdown',
|
|
|
|
content: this.getTruncatedDescription(),
|
|
|
|
icon: {
|
|
|
|
type: 'Image',
|
|
|
|
url: getThumbnailUrl(this, baseUrlHttp),
|
|
|
|
mediaType: 'image/jpeg',
|
|
|
|
width: THUMBNAILS_SIZE.width,
|
|
|
|
height: THUMBNAILS_SIZE.height
|
|
|
|
},
|
|
|
|
url
|
2016-12-29 12:07:05 -06:00
|
|
|
}
|
|
|
|
|
2017-11-09 10:51:58 -06:00
|
|
|
return videoObject
|
2016-12-29 12:07:05 -06:00
|
|
|
}
|
|
|
|
|
2017-10-30 04:16:27 -05:00
|
|
|
getTruncatedDescription = function (this: VideoInstance) {
|
|
|
|
const options = {
|
|
|
|
length: CONSTRAINTS_FIELDS.VIDEOS.TRUNCATED_DESCRIPTION.max
|
|
|
|
}
|
|
|
|
|
|
|
|
return truncate(this.description, options)
|
|
|
|
}
|
|
|
|
|
2017-11-09 10:51:58 -06:00
|
|
|
optimizeOriginalVideofile = async function (this: VideoInstance) {
|
2017-05-15 15:22:03 -05:00
|
|
|
const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
|
2017-05-02 15:02:27 -05:00
|
|
|
const newExtname = '.mp4'
|
2017-10-02 05:20:26 -05:00
|
|
|
const inputVideoFile = this.getOriginalFile()
|
2017-08-25 04:36:23 -05:00
|
|
|
const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
|
|
|
|
const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
|
2017-05-02 15:02:27 -05:00
|
|
|
|
2017-10-09 04:06:13 -05:00
|
|
|
const transcodeOptions = {
|
|
|
|
inputPath: videoInputPath,
|
|
|
|
outputPath: videoOutputPath
|
|
|
|
}
|
|
|
|
|
2017-11-09 10:51:58 -06:00
|
|
|
try {
|
|
|
|
// Could be very long!
|
|
|
|
await transcode(transcodeOptions)
|
2017-10-09 04:06:13 -05:00
|
|
|
|
2017-11-09 10:51:58 -06:00
|
|
|
await unlinkPromise(videoInputPath)
|
2017-10-09 04:06:13 -05:00
|
|
|
|
2017-11-09 10:51:58 -06:00
|
|
|
// Important to do this before getVideoFilename() to take in account the new file extension
|
|
|
|
inputVideoFile.set('extname', newExtname)
|
|
|
|
|
|
|
|
await renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile))
|
|
|
|
const stats = await statPromise(this.getVideoFilePath(inputVideoFile))
|
|
|
|
|
|
|
|
inputVideoFile.set('size', stats.size)
|
|
|
|
|
|
|
|
await this.createTorrentAndSetInfoHash(inputVideoFile)
|
|
|
|
await inputVideoFile.save()
|
|
|
|
|
|
|
|
} catch (err) {
|
|
|
|
// Auto destruction...
|
|
|
|
this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
|
|
|
|
|
|
|
|
throw err
|
|
|
|
}
|
2017-05-02 15:02:27 -05:00
|
|
|
}
|
|
|
|
|
2017-11-09 10:51:58 -06:00
|
|
|
transcodeOriginalVideofile = async function (this: VideoInstance, resolution: VideoResolution) {
|
2017-10-02 05:20:26 -05:00
|
|
|
const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
|
|
|
|
const extname = '.mp4'
|
|
|
|
|
|
|
|
// We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
|
|
|
|
const videoInputPath = join(videosDirectory, this.getVideoFilename(this.getOriginalFile()))
|
|
|
|
|
|
|
|
const newVideoFile = (Video['sequelize'].models.VideoFile as VideoFileModel).build({
|
|
|
|
resolution,
|
|
|
|
extname,
|
|
|
|
size: 0,
|
|
|
|
videoId: this.id
|
|
|
|
})
|
|
|
|
const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile))
|
2017-10-09 04:06:13 -05:00
|
|
|
|
|
|
|
const transcodeOptions = {
|
|
|
|
inputPath: videoInputPath,
|
|
|
|
outputPath: videoOutputPath,
|
|
|
|
resolution
|
|
|
|
}
|
|
|
|
|
2017-11-09 10:51:58 -06:00
|
|
|
await transcode(transcodeOptions)
|
|
|
|
|
|
|
|
const stats = await statPromise(videoOutputPath)
|
|
|
|
|
|
|
|
newVideoFile.set('size', stats.size)
|
|
|
|
|
|
|
|
await this.createTorrentAndSetInfoHash(newVideoFile)
|
|
|
|
|
|
|
|
await newVideoFile.save()
|
|
|
|
|
|
|
|
this.VideoFiles.push(newVideoFile)
|
2017-10-02 05:20:26 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
getOriginalFileHeight = function (this: VideoInstance) {
|
|
|
|
const originalFilePath = this.getVideoFilePath(this.getOriginalFile())
|
|
|
|
|
2017-10-09 04:06:13 -05:00
|
|
|
return getVideoFileHeight(originalFilePath)
|
2017-10-02 05:20:26 -05:00
|
|
|
}
|
|
|
|
|
2017-10-30 04:16:27 -05:00
|
|
|
getDescriptionPath = function (this: VideoInstance) {
|
|
|
|
return `/api/${API_VERSION}/videos/${this.uuid}/description`
|
|
|
|
}
|
|
|
|
|
2017-11-09 10:51:58 -06:00
|
|
|
getCategoryLabel = function (this: VideoInstance) {
|
|
|
|
let categoryLabel = VIDEO_CATEGORIES[this.category]
|
|
|
|
|
|
|
|
// Maybe our pod is not up to date and there are new categories since our version
|
|
|
|
if (!categoryLabel) categoryLabel = 'Misc'
|
|
|
|
|
|
|
|
return categoryLabel
|
|
|
|
}
|
|
|
|
|
|
|
|
getLicenceLabel = function (this: VideoInstance) {
|
|
|
|
let licenceLabel = VIDEO_LICENCES[this.licence]
|
2017-11-10 07:34:45 -06:00
|
|
|
|
2017-11-09 10:51:58 -06:00
|
|
|
// Maybe our pod is not up to date and there are new licences since our version
|
|
|
|
if (!licenceLabel) licenceLabel = 'Unknown'
|
|
|
|
|
|
|
|
return licenceLabel
|
|
|
|
}
|
|
|
|
|
|
|
|
getLanguageLabel = function (this: VideoInstance) {
|
|
|
|
// Language is an optional attribute
|
|
|
|
let languageLabel = VIDEO_LANGUAGES[this.language]
|
|
|
|
if (!languageLabel) languageLabel = 'Unknown'
|
|
|
|
|
|
|
|
return languageLabel
|
|
|
|
}
|
|
|
|
|
2017-08-25 04:36:23 -05:00
|
|
|
removeThumbnail = function (this: VideoInstance) {
|
|
|
|
const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
|
|
|
|
return unlinkPromise(thumbnailPath)
|
|
|
|
}
|
|
|
|
|
|
|
|
removePreview = function (this: VideoInstance) {
|
|
|
|
// Same name than video thumbnail
|
|
|
|
return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName())
|
|
|
|
}
|
|
|
|
|
|
|
|
removeFile = function (this: VideoInstance, videoFile: VideoFileInstance) {
|
|
|
|
const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
|
|
|
|
return unlinkPromise(filePath)
|
|
|
|
}
|
|
|
|
|
|
|
|
removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) {
|
2017-09-04 13:07:54 -05:00
|
|
|
const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
|
|
|
|
return unlinkPromise(torrentPath)
|
2017-08-25 04:36:23 -05:00
|
|
|
}
|
|
|
|
|
2016-06-24 10:42:51 -05:00
|
|
|
// ------------------------------ STATICS ------------------------------
|
|
|
|
|
2017-07-05 06:26:25 -05:00
|
|
|
generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string) {
|
2016-11-16 14:16:41 -06:00
|
|
|
// Creating the thumbnail for a remote video
|
|
|
|
|
|
|
|
const thumbnailName = video.getThumbnailName()
|
2017-05-15 15:22:03 -05:00
|
|
|
const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
|
2017-07-05 06:26:25 -05:00
|
|
|
return writeFilePromise(thumbnailPath, Buffer.from(thumbnailData, 'binary')).then(() => {
|
|
|
|
return thumbnailName
|
2016-11-16 14:16:41 -06:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2017-07-05 06:26:25 -05:00
|
|
|
list = function () {
|
2017-08-25 04:36:23 -05:00
|
|
|
const query = {
|
|
|
|
include: [ Video['sequelize'].models.VideoFile ]
|
|
|
|
}
|
|
|
|
|
|
|
|
return Video.findAll(query)
|
2016-12-25 02:44:57 -06:00
|
|
|
}
|
|
|
|
|
2017-10-31 05:52:52 -05:00
|
|
|
listUserVideosForApi = function (userId: number, start: number, count: number, sort: string) {
|
|
|
|
const query = {
|
|
|
|
distinct: true,
|
|
|
|
offset: start,
|
|
|
|
limit: count,
|
|
|
|
order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
|
|
|
|
include: [
|
|
|
|
{
|
|
|
|
model: Video['sequelize'].models.VideoChannel,
|
|
|
|
required: true,
|
|
|
|
include: [
|
|
|
|
{
|
2017-11-09 10:51:58 -06:00
|
|
|
model: Video['sequelize'].models.Account,
|
2017-10-31 05:52:52 -05:00
|
|
|
where: {
|
|
|
|
userId
|
|
|
|
},
|
|
|
|
required: true
|
|
|
|
}
|
|
|
|
]
|
|
|
|
},
|
|
|
|
Video['sequelize'].models.Tag
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
return Video.findAndCountAll(query).then(({ rows, count }) => {
|
|
|
|
return {
|
|
|
|
data: rows,
|
|
|
|
total: count
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2017-07-05 06:26:25 -05:00
|
|
|
listForApi = function (start: number, count: number, sort: string) {
|
2016-12-11 14:50:51 -06:00
|
|
|
const query = {
|
2017-05-22 13:58:25 -05:00
|
|
|
distinct: true,
|
2016-12-11 14:50:51 -06:00
|
|
|
offset: start,
|
|
|
|
limit: count,
|
2017-05-22 13:58:25 -05:00
|
|
|
order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
|
2016-12-11 14:50:51 -06:00
|
|
|
include: [
|
|
|
|
{
|
2017-10-24 12:41:09 -05:00
|
|
|
model: Video['sequelize'].models.VideoChannel,
|
|
|
|
include: [
|
|
|
|
{
|
2017-11-09 10:51:58 -06:00
|
|
|
model: Video['sequelize'].models.Account,
|
2017-10-24 12:41:09 -05:00
|
|
|
include: [
|
|
|
|
{
|
|
|
|
model: Video['sequelize'].models.Pod,
|
|
|
|
required: false
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
]
|
2016-12-24 09:59:17 -06:00
|
|
|
},
|
2017-10-31 05:52:52 -05:00
|
|
|
Video['sequelize'].models.Tag
|
2017-04-26 14:22:10 -05:00
|
|
|
],
|
2017-05-22 13:58:25 -05:00
|
|
|
where: createBaseVideosWhere()
|
2016-12-11 14:50:51 -06:00
|
|
|
}
|
|
|
|
|
2017-07-05 06:26:25 -05:00
|
|
|
return Video.findAndCountAll(query).then(({ rows, count }) => {
|
|
|
|
return {
|
|
|
|
data: rows,
|
|
|
|
total: count
|
|
|
|
}
|
2016-12-11 14:50:51 -06:00
|
|
|
})
|
2016-06-24 10:42:51 -05:00
|
|
|
}
|
|
|
|
|
2017-10-24 12:41:09 -05:00
|
|
|
loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Transaction) {
|
|
|
|
const query: Sequelize.FindOptions<VideoAttributes> = {
|
2016-12-11 14:50:51 -06:00
|
|
|
where: {
|
2017-07-11 09:01:56 -05:00
|
|
|
uuid
|
2016-12-11 14:50:51 -06:00
|
|
|
},
|
|
|
|
include: [
|
2017-08-25 04:36:23 -05:00
|
|
|
{
|
|
|
|
model: Video['sequelize'].models.VideoFile
|
|
|
|
},
|
2016-12-11 14:50:51 -06:00
|
|
|
{
|
2017-10-24 12:41:09 -05:00
|
|
|
model: Video['sequelize'].models.VideoChannel,
|
2016-12-11 14:50:51 -06:00
|
|
|
include: [
|
|
|
|
{
|
2017-11-09 10:51:58 -06:00
|
|
|
model: Video['sequelize'].models.Account,
|
2017-10-24 12:41:09 -05:00
|
|
|
include: [
|
|
|
|
{
|
|
|
|
model: Video['sequelize'].models.Pod,
|
|
|
|
required: true,
|
|
|
|
where: {
|
|
|
|
host: fromHost
|
|
|
|
}
|
|
|
|
}
|
|
|
|
]
|
2016-12-11 14:50:51 -06:00
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
2016-06-24 10:42:51 -05:00
|
|
|
|
2017-10-24 12:41:09 -05:00
|
|
|
if (t !== undefined) query.transaction = t
|
|
|
|
|
2017-07-05 06:26:25 -05:00
|
|
|
return Video.findOne(query)
|
2016-06-24 10:42:51 -05:00
|
|
|
}
|
|
|
|
|
2017-11-09 10:51:58 -06:00
|
|
|
listOwnedAndPopulateAccountAndTags = function () {
|
2016-12-11 14:50:51 -06:00
|
|
|
const query = {
|
|
|
|
where: {
|
2017-07-11 09:01:56 -05:00
|
|
|
remote: false
|
2016-12-11 14:50:51 -06:00
|
|
|
},
|
2017-08-25 04:36:23 -05:00
|
|
|
include: [
|
|
|
|
Video['sequelize'].models.VideoFile,
|
2017-10-24 12:41:09 -05:00
|
|
|
{
|
|
|
|
model: Video['sequelize'].models.VideoChannel,
|
2017-11-09 10:51:58 -06:00
|
|
|
include: [ Video['sequelize'].models.Account ]
|
2017-10-24 12:41:09 -05:00
|
|
|
},
|
2017-08-25 04:36:23 -05:00
|
|
|
Video['sequelize'].models.Tag
|
|
|
|
]
|
2016-12-11 14:50:51 -06:00
|
|
|
}
|
|
|
|
|
2017-07-05 06:26:25 -05:00
|
|
|
return Video.findAll(query)
|
2016-06-24 10:42:51 -05:00
|
|
|
}
|
|
|
|
|
2017-11-09 10:51:58 -06:00
|
|
|
listOwnedByAccount = function (account: string) {
|
2016-12-11 14:50:51 -06:00
|
|
|
const query = {
|
|
|
|
where: {
|
2017-07-11 09:01:56 -05:00
|
|
|
remote: false
|
2016-12-11 14:50:51 -06:00
|
|
|
},
|
|
|
|
include: [
|
2017-08-25 04:36:23 -05:00
|
|
|
{
|
|
|
|
model: Video['sequelize'].models.VideoFile
|
|
|
|
},
|
2016-12-11 14:50:51 -06:00
|
|
|
{
|
2017-10-24 12:41:09 -05:00
|
|
|
model: Video['sequelize'].models.VideoChannel,
|
|
|
|
include: [
|
|
|
|
{
|
2017-11-09 10:51:58 -06:00
|
|
|
model: Video['sequelize'].models.Account,
|
2017-10-24 12:41:09 -05:00
|
|
|
where: {
|
2017-11-09 10:51:58 -06:00
|
|
|
name: account
|
2017-10-24 12:41:09 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
]
|
2016-12-11 14:50:51 -06:00
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
2016-08-04 15:32:36 -05:00
|
|
|
|
2017-07-05 06:26:25 -05:00
|
|
|
return Video.findAll(query)
|
2016-06-24 10:42:51 -05:00
|
|
|
}
|
|
|
|
|
2017-07-11 09:01:56 -05:00
|
|
|
load = function (id: number) {
|
2017-07-05 06:26:25 -05:00
|
|
|
return Video.findById(id)
|
2016-12-11 14:50:51 -06:00
|
|
|
}
|
|
|
|
|
2017-10-24 12:41:09 -05:00
|
|
|
loadByUUID = function (uuid: string, t?: Sequelize.Transaction) {
|
|
|
|
const query: Sequelize.FindOptions<VideoAttributes> = {
|
2017-07-11 09:01:56 -05:00
|
|
|
where: {
|
|
|
|
uuid
|
2017-08-25 04:36:23 -05:00
|
|
|
},
|
|
|
|
include: [ Video['sequelize'].models.VideoFile ]
|
2017-10-26 04:26:35 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if (t !== undefined) query.transaction = t
|
|
|
|
|
|
|
|
return Video.findOne(query)
|
|
|
|
}
|
|
|
|
|
2017-11-10 07:34:45 -06:00
|
|
|
loadByUUIDOrURL = function (uuid: string, url: string, t?: Sequelize.Transaction) {
|
|
|
|
const query: Sequelize.FindOptions<VideoAttributes> = {
|
|
|
|
where: {
|
|
|
|
[Sequelize.Op.or]: [
|
|
|
|
{ uuid },
|
|
|
|
{ url }
|
|
|
|
]
|
|
|
|
},
|
|
|
|
include: [ Video['sequelize'].models.VideoFile ]
|
|
|
|
}
|
|
|
|
|
|
|
|
if (t !== undefined) query.transaction = t
|
|
|
|
|
|
|
|
return Video.findOne(query)
|
|
|
|
}
|
|
|
|
|
2017-10-26 04:26:35 -05:00
|
|
|
loadLocalVideoByUUID = function (uuid: string, t?: Sequelize.Transaction) {
|
|
|
|
const query: Sequelize.FindOptions<VideoAttributes> = {
|
|
|
|
where: {
|
|
|
|
uuid,
|
|
|
|
remote: false
|
|
|
|
},
|
|
|
|
include: [ Video['sequelize'].models.VideoFile ]
|
2017-07-11 09:01:56 -05:00
|
|
|
}
|
2017-10-24 12:41:09 -05:00
|
|
|
|
|
|
|
if (t !== undefined) query.transaction = t
|
|
|
|
|
2017-07-11 09:01:56 -05:00
|
|
|
return Video.findOne(query)
|
|
|
|
}
|
|
|
|
|
2017-11-09 10:51:58 -06:00
|
|
|
loadAndPopulateAccount = function (id: number) {
|
2016-12-11 14:50:51 -06:00
|
|
|
const options = {
|
2017-10-24 12:41:09 -05:00
|
|
|
include: [
|
|
|
|
Video['sequelize'].models.VideoFile,
|
|
|
|
{
|
|
|
|
model: Video['sequelize'].models.VideoChannel,
|
2017-11-09 10:51:58 -06:00
|
|
|
include: [ Video['sequelize'].models.Account ]
|
2017-10-24 12:41:09 -05:00
|
|
|
}
|
|
|
|
]
|
2016-12-11 14:50:51 -06:00
|
|
|
}
|
|
|
|
|
2017-07-05 06:26:25 -05:00
|
|
|
return Video.findById(id, options)
|
2016-12-11 14:50:51 -06:00
|
|
|
}
|
|
|
|
|
2017-11-09 10:51:58 -06:00
|
|
|
loadAndPopulateAccountAndPodAndTags = function (id: number) {
|
2016-12-11 14:50:51 -06:00
|
|
|
const options = {
|
|
|
|
include: [
|
|
|
|
{
|
2017-10-24 12:41:09 -05:00
|
|
|
model: Video['sequelize'].models.VideoChannel,
|
|
|
|
include: [
|
|
|
|
{
|
2017-11-09 10:51:58 -06:00
|
|
|
model: Video['sequelize'].models.Account,
|
2017-10-24 12:41:09 -05:00
|
|
|
include: [ { model: Video['sequelize'].models.Pod, required: false } ]
|
|
|
|
}
|
|
|
|
]
|
2016-12-24 09:59:17 -06:00
|
|
|
},
|
2017-08-25 04:36:23 -05:00
|
|
|
Video['sequelize'].models.Tag,
|
|
|
|
Video['sequelize'].models.VideoFile
|
2016-12-11 14:50:51 -06:00
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2017-07-05 06:26:25 -05:00
|
|
|
return Video.findById(id, options)
|
2016-06-24 10:42:51 -05:00
|
|
|
}
|
|
|
|
|
2017-11-09 10:51:58 -06:00
|
|
|
loadByUUIDAndPopulateAccountAndPodAndTags = function (uuid: string) {
|
2017-07-11 09:01:56 -05:00
|
|
|
const options = {
|
|
|
|
where: {
|
|
|
|
uuid
|
|
|
|
},
|
|
|
|
include: [
|
|
|
|
{
|
2017-10-24 12:41:09 -05:00
|
|
|
model: Video['sequelize'].models.VideoChannel,
|
|
|
|
include: [
|
|
|
|
{
|
2017-11-09 10:51:58 -06:00
|
|
|
model: Video['sequelize'].models.Account,
|
2017-10-24 12:41:09 -05:00
|
|
|
include: [ { model: Video['sequelize'].models.Pod, required: false } ]
|
|
|
|
}
|
|
|
|
]
|
2017-07-11 09:01:56 -05:00
|
|
|
},
|
2017-08-25 04:36:23 -05:00
|
|
|
Video['sequelize'].models.Tag,
|
|
|
|
Video['sequelize'].models.VideoFile
|
2017-07-11 09:01:56 -05:00
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
return Video.findOne(options)
|
|
|
|
}
|
|
|
|
|
2017-11-09 10:51:58 -06:00
|
|
|
searchAndPopulateAccountAndPodAndTags = function (value: string, field: string, start: number, count: number, sort: string) {
|
2017-07-11 03:59:13 -05:00
|
|
|
const podInclude: Sequelize.IncludeOptions = {
|
2017-05-22 13:58:25 -05:00
|
|
|
model: Video['sequelize'].models.Pod,
|
2016-12-24 09:59:17 -06:00
|
|
|
required: false
|
2016-12-11 14:50:51 -06:00
|
|
|
}
|
2016-12-24 09:59:17 -06:00
|
|
|
|
2017-11-09 10:51:58 -06:00
|
|
|
const accountInclude: Sequelize.IncludeOptions = {
|
|
|
|
model: Video['sequelize'].models.Account,
|
2017-10-24 12:41:09 -05:00
|
|
|
include: [ podInclude ]
|
|
|
|
}
|
|
|
|
|
|
|
|
const videoChannelInclude: Sequelize.IncludeOptions = {
|
|
|
|
model: Video['sequelize'].models.VideoChannel,
|
2017-11-09 10:51:58 -06:00
|
|
|
include: [ accountInclude ],
|
2017-10-24 12:41:09 -05:00
|
|
|
required: true
|
2016-12-11 14:50:51 -06:00
|
|
|
}
|
|
|
|
|
2017-07-11 03:59:13 -05:00
|
|
|
const tagInclude: Sequelize.IncludeOptions = {
|
2017-05-22 13:58:25 -05:00
|
|
|
model: Video['sequelize'].models.Tag
|
2016-12-24 09:59:17 -06:00
|
|
|
}
|
|
|
|
|
2017-08-25 11:36:49 -05:00
|
|
|
const query: Sequelize.FindOptions<VideoAttributes> = {
|
2017-05-22 13:58:25 -05:00
|
|
|
distinct: true,
|
|
|
|
where: createBaseVideosWhere(),
|
2016-12-11 14:50:51 -06:00
|
|
|
offset: start,
|
|
|
|
limit: count,
|
2017-05-22 13:58:25 -05:00
|
|
|
order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ]
|
2016-12-11 14:50:51 -06:00
|
|
|
}
|
|
|
|
|
2017-10-31 05:52:52 -05:00
|
|
|
if (field === 'tags') {
|
2017-05-22 13:58:25 -05:00
|
|
|
const escapedValue = Video['sequelize'].escape('%' + value + '%')
|
2017-10-26 09:59:02 -05:00
|
|
|
query.where['id'][Sequelize.Op.in] = Video['sequelize'].literal(
|
2017-07-05 06:26:25 -05:00
|
|
|
`(SELECT "VideoTags"."videoId"
|
|
|
|
FROM "Tags"
|
|
|
|
INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId"
|
2017-07-06 11:01:02 -05:00
|
|
|
WHERE name ILIKE ${escapedValue}
|
2017-07-05 06:26:25 -05:00
|
|
|
)`
|
2017-04-26 14:22:10 -05:00
|
|
|
)
|
2016-12-24 09:59:17 -06:00
|
|
|
} else if (field === 'host') {
|
|
|
|
// FIXME: Include our pod? (not stored in the database)
|
|
|
|
podInclude.where = {
|
|
|
|
host: {
|
2017-10-26 09:59:02 -05:00
|
|
|
[Sequelize.Op.iLike]: '%' + value + '%'
|
2016-12-11 14:50:51 -06:00
|
|
|
}
|
|
|
|
}
|
2016-12-24 09:59:17 -06:00
|
|
|
podInclude.required = true
|
2017-11-09 10:51:58 -06:00
|
|
|
} else if (field === 'account') {
|
|
|
|
accountInclude.where = {
|
2016-12-24 09:59:17 -06:00
|
|
|
name: {
|
2017-10-26 09:59:02 -05:00
|
|
|
[Sequelize.Op.iLike]: '%' + value + '%'
|
2016-12-11 14:50:51 -06:00
|
|
|
}
|
|
|
|
}
|
2016-06-24 10:42:51 -05:00
|
|
|
} else {
|
2016-12-11 14:50:51 -06:00
|
|
|
query.where[field] = {
|
2017-10-26 09:59:02 -05:00
|
|
|
[Sequelize.Op.iLike]: '%' + value + '%'
|
2016-12-11 14:50:51 -06:00
|
|
|
}
|
2016-06-24 10:42:51 -05:00
|
|
|
}
|
|
|
|
|
2016-12-24 09:59:17 -06:00
|
|
|
query.include = [
|
2017-10-31 05:52:52 -05:00
|
|
|
videoChannelInclude, tagInclude
|
2016-12-24 09:59:17 -06:00
|
|
|
]
|
|
|
|
|
2017-07-05 06:26:25 -05:00
|
|
|
return Video.findAndCountAll(query).then(({ rows, count }) => {
|
|
|
|
return {
|
|
|
|
data: rows,
|
|
|
|
total: count
|
|
|
|
}
|
2016-12-11 14:50:51 -06:00
|
|
|
})
|
2016-06-24 10:42:51 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
2017-05-01 12:09:55 -05:00
|
|
|
function createBaseVideosWhere () {
|
|
|
|
return {
|
|
|
|
id: {
|
2017-10-26 09:59:02 -05:00
|
|
|
[Sequelize.Op.notIn]: Video['sequelize'].literal(
|
2017-05-01 12:09:55 -05:00
|
|
|
'(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")'
|
|
|
|
)
|
2017-10-31 05:52:52 -05:00
|
|
|
},
|
|
|
|
privacy: VideoPrivacy.PUBLIC
|
2017-05-01 12:09:55 -05:00
|
|
|
}
|
|
|
|
}
|
2017-10-19 07:58:28 -05:00
|
|
|
|
|
|
|
function getBaseUrls (video: VideoInstance) {
|
|
|
|
let baseUrlHttp
|
|
|
|
let baseUrlWs
|
|
|
|
|
|
|
|
if (video.isOwned()) {
|
|
|
|
baseUrlHttp = CONFIG.WEBSERVER.URL
|
|
|
|
baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
|
|
|
|
} else {
|
2017-11-09 10:51:58 -06:00
|
|
|
baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + video.VideoChannel.Account.Pod.host
|
|
|
|
baseUrlWs = REMOTE_SCHEME.WS + '://' + video.VideoChannel.Account.Pod.host
|
2017-10-19 07:58:28 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
return { baseUrlHttp, baseUrlWs }
|
|
|
|
}
|
|
|
|
|
2017-11-09 10:51:58 -06:00
|
|
|
function getThumbnailUrl (video: VideoInstance, baseUrlHttp: string) {
|
|
|
|
return baseUrlHttp + STATIC_PATHS.THUMBNAILS + video.getThumbnailName()
|
|
|
|
}
|
|
|
|
|
2017-10-19 07:58:28 -05:00
|
|
|
function getTorrentUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) {
|
|
|
|
return baseUrlHttp + STATIC_PATHS.TORRENTS + video.getTorrentFileName(videoFile)
|
|
|
|
}
|
|
|
|
|
|
|
|
function getVideoFileUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) {
|
|
|
|
return baseUrlHttp + STATIC_PATHS.WEBSEED + video.getVideoFilename(videoFile)
|
|
|
|
}
|
|
|
|
|
|
|
|
function generateMagnetUri (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string, baseUrlWs: string) {
|
|
|
|
const xs = getTorrentUrl(video, videoFile, baseUrlHttp)
|
|
|
|
const announce = [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
|
|
|
|
const urlList = [ getVideoFileUrl(video, videoFile, baseUrlHttp) ]
|
|
|
|
|
|
|
|
const magnetHash = {
|
|
|
|
xs,
|
|
|
|
announce,
|
|
|
|
urlList,
|
|
|
|
infoHash: videoFile.infoHash,
|
|
|
|
name: video.name
|
|
|
|
}
|
|
|
|
|
|
|
|
return magnetUtil.encode(magnetHash)
|
|
|
|
}
|