Reduce video.ts file size by moving some methods in other files
This commit is contained in:
parent
df182b373f
commit
098eb37797
|
@ -32,7 +32,7 @@ redundancy:
|
||||||
-
|
-
|
||||||
size: '10MB'
|
size: '10MB'
|
||||||
strategy: 'recently-added'
|
strategy: 'recently-added'
|
||||||
minViews: 10
|
minViews: 1
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
previews:
|
previews:
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { VideoModel } from '../models/video/video'
|
||||||
import * as validator from 'validator'
|
import * as validator from 'validator'
|
||||||
import { VideoPrivacy } from '../../shared/models/videos'
|
import { VideoPrivacy } from '../../shared/models/videos'
|
||||||
import { readFile } from 'fs-extra'
|
import { readFile } from 'fs-extra'
|
||||||
|
import { getActivityStreamDuration } from '../models/video/video-format-utils'
|
||||||
|
|
||||||
export class ClientHtml {
|
export class ClientHtml {
|
||||||
|
|
||||||
|
@ -150,7 +151,7 @@ export class ClientHtml {
|
||||||
description: videoDescriptionEscaped,
|
description: videoDescriptionEscaped,
|
||||||
thumbnailUrl: previewUrl,
|
thumbnailUrl: previewUrl,
|
||||||
uploadDate: video.createdAt.toISOString(),
|
uploadDate: video.createdAt.toISOString(),
|
||||||
duration: video.getActivityStreamDuration(),
|
duration: getActivityStreamDuration(video.duration),
|
||||||
contentUrl: videoUrl,
|
contentUrl: videoUrl,
|
||||||
embedUrl: embedUrl,
|
embedUrl: embedUrl,
|
||||||
interactionCount: video.views
|
interactionCount: video.views
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { retryTransactionWrapper } from '../../../helpers/database-utils'
|
||||||
import { sequelizeTypescript } from '../../../initializers'
|
import { sequelizeTypescript } from '../../../initializers'
|
||||||
import * as Bluebird from 'bluebird'
|
import * as Bluebird from 'bluebird'
|
||||||
import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils'
|
import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils'
|
||||||
|
import { importVideoFile, transcodeOriginalVideofile, optimizeOriginalVideofile } from '../../video-transcoding'
|
||||||
|
|
||||||
export type VideoFilePayload = {
|
export type VideoFilePayload = {
|
||||||
videoUUID: string
|
videoUUID: string
|
||||||
|
@ -32,7 +33,7 @@ async function processVideoFileImport (job: Bull.Job) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
await video.importVideoFile(payload.filePath)
|
await importVideoFile(video, payload.filePath)
|
||||||
|
|
||||||
await onVideoFileTranscoderOrImportSuccess(video)
|
await onVideoFileTranscoderOrImportSuccess(video)
|
||||||
return video
|
return video
|
||||||
|
@ -51,11 +52,11 @@ async function processVideoFile (job: Bull.Job) {
|
||||||
|
|
||||||
// Transcoding in other resolution
|
// Transcoding in other resolution
|
||||||
if (payload.resolution) {
|
if (payload.resolution) {
|
||||||
await video.transcodeOriginalVideofile(payload.resolution, payload.isPortraitMode || false)
|
await transcodeOriginalVideofile(video, payload.resolution, payload.isPortraitMode || false)
|
||||||
|
|
||||||
await retryTransactionWrapper(onVideoFileTranscoderOrImportSuccess, video)
|
await retryTransactionWrapper(onVideoFileTranscoderOrImportSuccess, video)
|
||||||
} else {
|
} else {
|
||||||
await video.optimizeOriginalVideofile()
|
await optimizeOriginalVideofile(video)
|
||||||
|
|
||||||
await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload.isNewVideo)
|
await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload.isNewVideo)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { CONFIG } from '../initializers'
|
||||||
|
import { join, extname } from 'path'
|
||||||
|
import { getVideoFileFPS, getVideoFileResolution, transcode } from '../helpers/ffmpeg-utils'
|
||||||
|
import { copy, remove, rename, stat } from 'fs-extra'
|
||||||
|
import { logger } from '../helpers/logger'
|
||||||
|
import { VideoResolution } from '../../shared/models/videos'
|
||||||
|
import { VideoFileModel } from '../models/video/video-file'
|
||||||
|
import { VideoModel } from '../models/video/video'
|
||||||
|
|
||||||
|
async function optimizeOriginalVideofile (video: VideoModel) {
|
||||||
|
const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
|
||||||
|
const newExtname = '.mp4'
|
||||||
|
const inputVideoFile = video.getOriginalFile()
|
||||||
|
const videoInputPath = join(videosDirectory, video.getVideoFilename(inputVideoFile))
|
||||||
|
const videoTranscodedPath = join(videosDirectory, video.id + '-transcoded' + newExtname)
|
||||||
|
|
||||||
|
const transcodeOptions = {
|
||||||
|
inputPath: videoInputPath,
|
||||||
|
outputPath: videoTranscodedPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// Could be very long!
|
||||||
|
await transcode(transcodeOptions)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await remove(videoInputPath)
|
||||||
|
|
||||||
|
// Important to do this before getVideoFilename() to take in account the new file extension
|
||||||
|
inputVideoFile.set('extname', newExtname)
|
||||||
|
|
||||||
|
const videoOutputPath = video.getVideoFilePath(inputVideoFile)
|
||||||
|
await rename(videoTranscodedPath, videoOutputPath)
|
||||||
|
const stats = await stat(videoOutputPath)
|
||||||
|
const fps = await getVideoFileFPS(videoOutputPath)
|
||||||
|
|
||||||
|
inputVideoFile.set('size', stats.size)
|
||||||
|
inputVideoFile.set('fps', fps)
|
||||||
|
|
||||||
|
await video.createTorrentAndSetInfoHash(inputVideoFile)
|
||||||
|
await inputVideoFile.save()
|
||||||
|
} catch (err) {
|
||||||
|
// Auto destruction...
|
||||||
|
video.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', { err }))
|
||||||
|
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoResolution, isPortraitMode: boolean) {
|
||||||
|
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, video.getVideoFilename(video.getOriginalFile()))
|
||||||
|
|
||||||
|
const newVideoFile = new VideoFileModel({
|
||||||
|
resolution,
|
||||||
|
extname,
|
||||||
|
size: 0,
|
||||||
|
videoId: video.id
|
||||||
|
})
|
||||||
|
const videoOutputPath = join(videosDirectory, video.getVideoFilename(newVideoFile))
|
||||||
|
|
||||||
|
const transcodeOptions = {
|
||||||
|
inputPath: videoInputPath,
|
||||||
|
outputPath: videoOutputPath,
|
||||||
|
resolution,
|
||||||
|
isPortraitMode
|
||||||
|
}
|
||||||
|
|
||||||
|
await transcode(transcodeOptions)
|
||||||
|
|
||||||
|
const stats = await stat(videoOutputPath)
|
||||||
|
const fps = await getVideoFileFPS(videoOutputPath)
|
||||||
|
|
||||||
|
newVideoFile.set('size', stats.size)
|
||||||
|
newVideoFile.set('fps', fps)
|
||||||
|
|
||||||
|
await video.createTorrentAndSetInfoHash(newVideoFile)
|
||||||
|
|
||||||
|
await newVideoFile.save()
|
||||||
|
|
||||||
|
video.VideoFiles.push(newVideoFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importVideoFile (video: VideoModel, inputFilePath: string) {
|
||||||
|
const { videoFileResolution } = await getVideoFileResolution(inputFilePath)
|
||||||
|
const { size } = await stat(inputFilePath)
|
||||||
|
const fps = await getVideoFileFPS(inputFilePath)
|
||||||
|
|
||||||
|
let updatedVideoFile = new VideoFileModel({
|
||||||
|
resolution: videoFileResolution,
|
||||||
|
extname: extname(inputFilePath),
|
||||||
|
size,
|
||||||
|
fps,
|
||||||
|
videoId: video.id
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentVideoFile = video.VideoFiles.find(videoFile => videoFile.resolution === updatedVideoFile.resolution)
|
||||||
|
|
||||||
|
if (currentVideoFile) {
|
||||||
|
// Remove old file and old torrent
|
||||||
|
await video.removeFile(currentVideoFile)
|
||||||
|
await video.removeTorrent(currentVideoFile)
|
||||||
|
// Remove the old video file from the array
|
||||||
|
video.VideoFiles = video.VideoFiles.filter(f => f !== currentVideoFile)
|
||||||
|
|
||||||
|
// Update the database
|
||||||
|
currentVideoFile.set('extname', updatedVideoFile.extname)
|
||||||
|
currentVideoFile.set('size', updatedVideoFile.size)
|
||||||
|
currentVideoFile.set('fps', updatedVideoFile.fps)
|
||||||
|
|
||||||
|
updatedVideoFile = currentVideoFile
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputPath = video.getVideoFilePath(updatedVideoFile)
|
||||||
|
await copy(inputFilePath, outputPath)
|
||||||
|
|
||||||
|
await video.createTorrentAndSetInfoHash(updatedVideoFile)
|
||||||
|
|
||||||
|
await updatedVideoFile.save()
|
||||||
|
|
||||||
|
video.VideoFiles.push(updatedVideoFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
optimizeOriginalVideofile,
|
||||||
|
transcodeOriginalVideofile,
|
||||||
|
importVideoFile
|
||||||
|
}
|
|
@ -193,7 +193,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
|
||||||
// On VideoModel!
|
// On VideoModel!
|
||||||
const query = {
|
const query = {
|
||||||
attributes: [ 'id', 'publishedAt' ],
|
attributes: [ 'id', 'publishedAt' ],
|
||||||
// logging: !isTestInstance(),
|
logging: !isTestInstance(),
|
||||||
limit: randomizedFactor,
|
limit: randomizedFactor,
|
||||||
order: getVideoSort('-publishedAt'),
|
order: getVideoSort('-publishedAt'),
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -0,0 +1,295 @@
|
||||||
|
import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
|
||||||
|
import { VideoModel } from './video'
|
||||||
|
import { VideoFileModel } from './video-file'
|
||||||
|
import { ActivityUrlObject, VideoTorrentObject } from '../../../shared/models/activitypub/objects'
|
||||||
|
import { CONFIG, THUMBNAILS_SIZE, VIDEO_EXT_MIMETYPE } from '../../initializers'
|
||||||
|
import { VideoCaptionModel } from './video-caption'
|
||||||
|
import {
|
||||||
|
getVideoCommentsActivityPubUrl,
|
||||||
|
getVideoDislikesActivityPubUrl,
|
||||||
|
getVideoLikesActivityPubUrl,
|
||||||
|
getVideoSharesActivityPubUrl
|
||||||
|
} from '../../lib/activitypub'
|
||||||
|
|
||||||
|
export type VideoFormattingJSONOptions = {
|
||||||
|
additionalAttributes: {
|
||||||
|
state?: boolean,
|
||||||
|
waitTranscoding?: boolean,
|
||||||
|
scheduledUpdate?: boolean,
|
||||||
|
blacklistInfo?: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormattingJSONOptions): Video {
|
||||||
|
const formattedAccount = video.VideoChannel.Account.toFormattedJSON()
|
||||||
|
const formattedVideoChannel = video.VideoChannel.toFormattedJSON()
|
||||||
|
|
||||||
|
const videoObject: Video = {
|
||||||
|
id: video.id,
|
||||||
|
uuid: video.uuid,
|
||||||
|
name: video.name,
|
||||||
|
category: {
|
||||||
|
id: video.category,
|
||||||
|
label: VideoModel.getCategoryLabel(video.category)
|
||||||
|
},
|
||||||
|
licence: {
|
||||||
|
id: video.licence,
|
||||||
|
label: VideoModel.getLicenceLabel(video.licence)
|
||||||
|
},
|
||||||
|
language: {
|
||||||
|
id: video.language,
|
||||||
|
label: VideoModel.getLanguageLabel(video.language)
|
||||||
|
},
|
||||||
|
privacy: {
|
||||||
|
id: video.privacy,
|
||||||
|
label: VideoModel.getPrivacyLabel(video.privacy)
|
||||||
|
},
|
||||||
|
nsfw: video.nsfw,
|
||||||
|
description: video.getTruncatedDescription(),
|
||||||
|
isLocal: video.isOwned(),
|
||||||
|
duration: video.duration,
|
||||||
|
views: video.views,
|
||||||
|
likes: video.likes,
|
||||||
|
dislikes: video.dislikes,
|
||||||
|
thumbnailPath: video.getThumbnailStaticPath(),
|
||||||
|
previewPath: video.getPreviewStaticPath(),
|
||||||
|
embedPath: video.getEmbedStaticPath(),
|
||||||
|
createdAt: video.createdAt,
|
||||||
|
updatedAt: video.updatedAt,
|
||||||
|
publishedAt: video.publishedAt,
|
||||||
|
account: {
|
||||||
|
id: formattedAccount.id,
|
||||||
|
uuid: formattedAccount.uuid,
|
||||||
|
name: formattedAccount.name,
|
||||||
|
displayName: formattedAccount.displayName,
|
||||||
|
url: formattedAccount.url,
|
||||||
|
host: formattedAccount.host,
|
||||||
|
avatar: formattedAccount.avatar
|
||||||
|
},
|
||||||
|
channel: {
|
||||||
|
id: formattedVideoChannel.id,
|
||||||
|
uuid: formattedVideoChannel.uuid,
|
||||||
|
name: formattedVideoChannel.name,
|
||||||
|
displayName: formattedVideoChannel.displayName,
|
||||||
|
url: formattedVideoChannel.url,
|
||||||
|
host: formattedVideoChannel.host,
|
||||||
|
avatar: formattedVideoChannel.avatar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options) {
|
||||||
|
if (options.additionalAttributes.state === true) {
|
||||||
|
videoObject.state = {
|
||||||
|
id: video.state,
|
||||||
|
label: VideoModel.getStateLabel(video.state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.additionalAttributes.waitTranscoding === true) {
|
||||||
|
videoObject.waitTranscoding = video.waitTranscoding
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.additionalAttributes.scheduledUpdate === true && video.ScheduleVideoUpdate) {
|
||||||
|
videoObject.scheduledUpdate = {
|
||||||
|
updateAt: video.ScheduleVideoUpdate.updateAt,
|
||||||
|
privacy: video.ScheduleVideoUpdate.privacy || undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.additionalAttributes.blacklistInfo === true) {
|
||||||
|
videoObject.blacklisted = !!video.VideoBlacklist
|
||||||
|
videoObject.blacklistedReason = video.VideoBlacklist ? video.VideoBlacklist.reason : null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return videoObject
|
||||||
|
}
|
||||||
|
|
||||||
|
function videoModelToFormattedDetailsJSON (video: VideoModel): VideoDetails {
|
||||||
|
const formattedJson = video.toFormattedJSON({
|
||||||
|
additionalAttributes: {
|
||||||
|
scheduledUpdate: true,
|
||||||
|
blacklistInfo: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const detailsJson = {
|
||||||
|
support: video.support,
|
||||||
|
descriptionPath: video.getDescriptionPath(),
|
||||||
|
channel: video.VideoChannel.toFormattedJSON(),
|
||||||
|
account: video.VideoChannel.Account.toFormattedJSON(),
|
||||||
|
tags: video.Tags.map(t => t.name),
|
||||||
|
commentsEnabled: video.commentsEnabled,
|
||||||
|
waitTranscoding: video.waitTranscoding,
|
||||||
|
state: {
|
||||||
|
id: video.state,
|
||||||
|
label: VideoModel.getStateLabel(video.state)
|
||||||
|
},
|
||||||
|
files: []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format and sort video files
|
||||||
|
detailsJson.files = videoFilesModelToFormattedJSON(video, video.VideoFiles)
|
||||||
|
|
||||||
|
return Object.assign(formattedJson, detailsJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
function videoFilesModelToFormattedJSON (video: VideoModel, videoFiles: VideoFileModel[]): VideoFile[] {
|
||||||
|
const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
|
||||||
|
|
||||||
|
return videoFiles
|
||||||
|
.map(videoFile => {
|
||||||
|
let resolutionLabel = videoFile.resolution + 'p'
|
||||||
|
|
||||||
|
return {
|
||||||
|
resolution: {
|
||||||
|
id: videoFile.resolution,
|
||||||
|
label: resolutionLabel
|
||||||
|
},
|
||||||
|
magnetUri: video.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs),
|
||||||
|
size: videoFile.size,
|
||||||
|
fps: videoFile.fps,
|
||||||
|
torrentUrl: video.getTorrentUrl(videoFile, baseUrlHttp),
|
||||||
|
torrentDownloadUrl: video.getTorrentDownloadUrl(videoFile, baseUrlHttp),
|
||||||
|
fileUrl: video.getVideoFileUrl(videoFile, baseUrlHttp),
|
||||||
|
fileDownloadUrl: video.getVideoFileDownloadUrl(videoFile, baseUrlHttp)
|
||||||
|
} as VideoFile
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.resolution.id < b.resolution.id) return 1
|
||||||
|
if (a.resolution.id === b.resolution.id) return 0
|
||||||
|
return -1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject {
|
||||||
|
const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
|
||||||
|
if (!video.Tags) video.Tags = []
|
||||||
|
|
||||||
|
const tag = video.Tags.map(t => ({
|
||||||
|
type: 'Hashtag' as 'Hashtag',
|
||||||
|
name: t.name
|
||||||
|
}))
|
||||||
|
|
||||||
|
let language
|
||||||
|
if (video.language) {
|
||||||
|
language = {
|
||||||
|
identifier: video.language,
|
||||||
|
name: VideoModel.getLanguageLabel(video.language)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let category
|
||||||
|
if (video.category) {
|
||||||
|
category = {
|
||||||
|
identifier: video.category + '',
|
||||||
|
name: VideoModel.getCategoryLabel(video.category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let licence
|
||||||
|
if (video.licence) {
|
||||||
|
licence = {
|
||||||
|
identifier: video.licence + '',
|
||||||
|
name: VideoModel.getLicenceLabel(video.licence)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const url: ActivityUrlObject[] = []
|
||||||
|
for (const file of video.VideoFiles) {
|
||||||
|
url.push({
|
||||||
|
type: 'Link',
|
||||||
|
mimeType: VIDEO_EXT_MIMETYPE[ file.extname ] as any,
|
||||||
|
href: video.getVideoFileUrl(file, baseUrlHttp),
|
||||||
|
height: file.resolution,
|
||||||
|
size: file.size,
|
||||||
|
fps: file.fps
|
||||||
|
})
|
||||||
|
|
||||||
|
url.push({
|
||||||
|
type: 'Link',
|
||||||
|
mimeType: 'application/x-bittorrent' as 'application/x-bittorrent',
|
||||||
|
href: video.getTorrentUrl(file, baseUrlHttp),
|
||||||
|
height: file.resolution
|
||||||
|
})
|
||||||
|
|
||||||
|
url.push({
|
||||||
|
type: 'Link',
|
||||||
|
mimeType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
|
||||||
|
href: video.generateMagnetUri(file, baseUrlHttp, baseUrlWs),
|
||||||
|
height: file.resolution
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add video url too
|
||||||
|
url.push({
|
||||||
|
type: 'Link',
|
||||||
|
mimeType: 'text/html',
|
||||||
|
href: CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid
|
||||||
|
})
|
||||||
|
|
||||||
|
const subtitleLanguage = []
|
||||||
|
for (const caption of video.VideoCaptions) {
|
||||||
|
subtitleLanguage.push({
|
||||||
|
identifier: caption.language,
|
||||||
|
name: VideoCaptionModel.getLanguageLabel(caption.language)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'Video' as 'Video',
|
||||||
|
id: video.url,
|
||||||
|
name: video.name,
|
||||||
|
duration: getActivityStreamDuration(video.duration),
|
||||||
|
uuid: video.uuid,
|
||||||
|
tag,
|
||||||
|
category,
|
||||||
|
licence,
|
||||||
|
language,
|
||||||
|
views: video.views,
|
||||||
|
sensitive: video.nsfw,
|
||||||
|
waitTranscoding: video.waitTranscoding,
|
||||||
|
state: video.state,
|
||||||
|
commentsEnabled: video.commentsEnabled,
|
||||||
|
published: video.publishedAt.toISOString(),
|
||||||
|
updated: video.updatedAt.toISOString(),
|
||||||
|
mediaType: 'text/markdown',
|
||||||
|
content: video.getTruncatedDescription(),
|
||||||
|
support: video.support,
|
||||||
|
subtitleLanguage,
|
||||||
|
icon: {
|
||||||
|
type: 'Image',
|
||||||
|
url: video.getThumbnailUrl(baseUrlHttp),
|
||||||
|
mediaType: 'image/jpeg',
|
||||||
|
width: THUMBNAILS_SIZE.width,
|
||||||
|
height: THUMBNAILS_SIZE.height
|
||||||
|
},
|
||||||
|
url,
|
||||||
|
likes: getVideoLikesActivityPubUrl(video),
|
||||||
|
dislikes: getVideoDislikesActivityPubUrl(video),
|
||||||
|
shares: getVideoSharesActivityPubUrl(video),
|
||||||
|
comments: getVideoCommentsActivityPubUrl(video),
|
||||||
|
attributedTo: [
|
||||||
|
{
|
||||||
|
type: 'Person',
|
||||||
|
id: video.VideoChannel.Account.Actor.url
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Group',
|
||||||
|
id: video.VideoChannel.Actor.url
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActivityStreamDuration (duration: number) {
|
||||||
|
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
|
||||||
|
return 'PT' + duration + 'S'
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
videoModelToFormattedJSON,
|
||||||
|
videoModelToFormattedDetailsJSON,
|
||||||
|
videoFilesModelToFormattedJSON,
|
||||||
|
videoModelToActivityPubObject,
|
||||||
|
getActivityStreamDuration
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
import * as Bluebird from 'bluebird'
|
import * as Bluebird from 'bluebird'
|
||||||
import { map, maxBy } from 'lodash'
|
import { maxBy } from 'lodash'
|
||||||
import * as magnetUtil from 'magnet-uri'
|
import * as magnetUtil from 'magnet-uri'
|
||||||
import * as parseTorrent from 'parse-torrent'
|
import * as parseTorrent from 'parse-torrent'
|
||||||
import { extname, join } from 'path'
|
import { join } from 'path'
|
||||||
import * as Sequelize from 'sequelize'
|
import * as Sequelize from 'sequelize'
|
||||||
import {
|
import {
|
||||||
AllowNull,
|
AllowNull,
|
||||||
|
@ -27,7 +27,7 @@ import {
|
||||||
Table,
|
Table,
|
||||||
UpdatedAt
|
UpdatedAt
|
||||||
} from 'sequelize-typescript'
|
} from 'sequelize-typescript'
|
||||||
import { ActivityUrlObject, VideoPrivacy, VideoResolution, VideoState } from '../../../shared'
|
import { VideoPrivacy, VideoState } from '../../../shared'
|
||||||
import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
|
import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
|
||||||
import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
|
import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
|
||||||
import { VideoFilter } from '../../../shared/models/videos/video-query.type'
|
import { VideoFilter } from '../../../shared/models/videos/video-query.type'
|
||||||
|
@ -45,7 +45,7 @@ import {
|
||||||
isVideoStateValid,
|
isVideoStateValid,
|
||||||
isVideoSupportValid
|
isVideoSupportValid
|
||||||
} from '../../helpers/custom-validators/videos'
|
} from '../../helpers/custom-validators/videos'
|
||||||
import { generateImageFromVideoFile, getVideoFileFPS, getVideoFileResolution, transcode } from '../../helpers/ffmpeg-utils'
|
import { generateImageFromVideoFile, getVideoFileResolution } from '../../helpers/ffmpeg-utils'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
import { getServerActor } from '../../helpers/utils'
|
import { getServerActor } from '../../helpers/utils'
|
||||||
import {
|
import {
|
||||||
|
@ -59,18 +59,11 @@ import {
|
||||||
STATIC_PATHS,
|
STATIC_PATHS,
|
||||||
THUMBNAILS_SIZE,
|
THUMBNAILS_SIZE,
|
||||||
VIDEO_CATEGORIES,
|
VIDEO_CATEGORIES,
|
||||||
VIDEO_EXT_MIMETYPE,
|
|
||||||
VIDEO_LANGUAGES,
|
VIDEO_LANGUAGES,
|
||||||
VIDEO_LICENCES,
|
VIDEO_LICENCES,
|
||||||
VIDEO_PRIVACIES,
|
VIDEO_PRIVACIES,
|
||||||
VIDEO_STATES
|
VIDEO_STATES
|
||||||
} from '../../initializers'
|
} from '../../initializers'
|
||||||
import {
|
|
||||||
getVideoCommentsActivityPubUrl,
|
|
||||||
getVideoDislikesActivityPubUrl,
|
|
||||||
getVideoLikesActivityPubUrl,
|
|
||||||
getVideoSharesActivityPubUrl
|
|
||||||
} from '../../lib/activitypub'
|
|
||||||
import { sendDeleteVideo } from '../../lib/activitypub/send'
|
import { sendDeleteVideo } from '../../lib/activitypub/send'
|
||||||
import { AccountModel } from '../account/account'
|
import { AccountModel } from '../account/account'
|
||||||
import { AccountVideoRateModel } from '../account/account-video-rate'
|
import { AccountVideoRateModel } from '../account/account-video-rate'
|
||||||
|
@ -88,9 +81,16 @@ import { VideoTagModel } from './video-tag'
|
||||||
import { ScheduleVideoUpdateModel } from './schedule-video-update'
|
import { ScheduleVideoUpdateModel } from './schedule-video-update'
|
||||||
import { VideoCaptionModel } from './video-caption'
|
import { VideoCaptionModel } from './video-caption'
|
||||||
import { VideoBlacklistModel } from './video-blacklist'
|
import { VideoBlacklistModel } from './video-blacklist'
|
||||||
import { copy, remove, rename, stat, writeFile } from 'fs-extra'
|
import { remove, writeFile } from 'fs-extra'
|
||||||
import { VideoViewModel } from './video-views'
|
import { VideoViewModel } from './video-views'
|
||||||
import { VideoRedundancyModel } from '../redundancy/video-redundancy'
|
import { VideoRedundancyModel } from '../redundancy/video-redundancy'
|
||||||
|
import {
|
||||||
|
videoFilesModelToFormattedJSON,
|
||||||
|
VideoFormattingJSONOptions,
|
||||||
|
videoModelToActivityPubObject,
|
||||||
|
videoModelToFormattedDetailsJSON,
|
||||||
|
videoModelToFormattedJSON
|
||||||
|
} from './video-format-utils'
|
||||||
|
|
||||||
// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
|
// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
|
||||||
const indexes: Sequelize.DefineIndexesOptions[] = [
|
const indexes: Sequelize.DefineIndexesOptions[] = [
|
||||||
|
@ -1257,23 +1257,23 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static getCategoryLabel (id: number) {
|
static getCategoryLabel (id: number) {
|
||||||
return VIDEO_CATEGORIES[ id ] || 'Misc'
|
return VIDEO_CATEGORIES[ id ] || 'Misc'
|
||||||
}
|
}
|
||||||
|
|
||||||
private static getLicenceLabel (id: number) {
|
static getLicenceLabel (id: number) {
|
||||||
return VIDEO_LICENCES[ id ] || 'Unknown'
|
return VIDEO_LICENCES[ id ] || 'Unknown'
|
||||||
}
|
}
|
||||||
|
|
||||||
private static getLanguageLabel (id: string) {
|
static getLanguageLabel (id: string) {
|
||||||
return VIDEO_LANGUAGES[ id ] || 'Unknown'
|
return VIDEO_LANGUAGES[ id ] || 'Unknown'
|
||||||
}
|
}
|
||||||
|
|
||||||
private static getPrivacyLabel (id: number) {
|
static getPrivacyLabel (id: number) {
|
||||||
return VIDEO_PRIVACIES[ id ] || 'Unknown'
|
return VIDEO_PRIVACIES[ id ] || 'Unknown'
|
||||||
}
|
}
|
||||||
|
|
||||||
private static getStateLabel (id: number) {
|
static getStateLabel (id: number) {
|
||||||
return VIDEO_STATES[ id ] || 'Unknown'
|
return VIDEO_STATES[ id ] || 'Unknown'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1369,273 +1369,20 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
return join(STATIC_PATHS.PREVIEWS, this.getPreviewName())
|
return join(STATIC_PATHS.PREVIEWS, this.getPreviewName())
|
||||||
}
|
}
|
||||||
|
|
||||||
toFormattedJSON (options?: {
|
toFormattedJSON (options?: VideoFormattingJSONOptions): Video {
|
||||||
additionalAttributes: {
|
return videoModelToFormattedJSON(this, options)
|
||||||
state?: boolean,
|
|
||||||
waitTranscoding?: boolean,
|
|
||||||
scheduledUpdate?: boolean,
|
|
||||||
blacklistInfo?: boolean
|
|
||||||
}
|
|
||||||
}): Video {
|
|
||||||
const formattedAccount = this.VideoChannel.Account.toFormattedJSON()
|
|
||||||
const formattedVideoChannel = this.VideoChannel.toFormattedJSON()
|
|
||||||
|
|
||||||
const videoObject: Video = {
|
|
||||||
id: this.id,
|
|
||||||
uuid: this.uuid,
|
|
||||||
name: this.name,
|
|
||||||
category: {
|
|
||||||
id: this.category,
|
|
||||||
label: VideoModel.getCategoryLabel(this.category)
|
|
||||||
},
|
|
||||||
licence: {
|
|
||||||
id: this.licence,
|
|
||||||
label: VideoModel.getLicenceLabel(this.licence)
|
|
||||||
},
|
|
||||||
language: {
|
|
||||||
id: this.language,
|
|
||||||
label: VideoModel.getLanguageLabel(this.language)
|
|
||||||
},
|
|
||||||
privacy: {
|
|
||||||
id: this.privacy,
|
|
||||||
label: VideoModel.getPrivacyLabel(this.privacy)
|
|
||||||
},
|
|
||||||
nsfw: this.nsfw,
|
|
||||||
description: this.getTruncatedDescription(),
|
|
||||||
isLocal: this.isOwned(),
|
|
||||||
duration: this.duration,
|
|
||||||
views: this.views,
|
|
||||||
likes: this.likes,
|
|
||||||
dislikes: this.dislikes,
|
|
||||||
thumbnailPath: this.getThumbnailStaticPath(),
|
|
||||||
previewPath: this.getPreviewStaticPath(),
|
|
||||||
embedPath: this.getEmbedStaticPath(),
|
|
||||||
createdAt: this.createdAt,
|
|
||||||
updatedAt: this.updatedAt,
|
|
||||||
publishedAt: this.publishedAt,
|
|
||||||
account: {
|
|
||||||
id: formattedAccount.id,
|
|
||||||
uuid: formattedAccount.uuid,
|
|
||||||
name: formattedAccount.name,
|
|
||||||
displayName: formattedAccount.displayName,
|
|
||||||
url: formattedAccount.url,
|
|
||||||
host: formattedAccount.host,
|
|
||||||
avatar: formattedAccount.avatar
|
|
||||||
},
|
|
||||||
channel: {
|
|
||||||
id: formattedVideoChannel.id,
|
|
||||||
uuid: formattedVideoChannel.uuid,
|
|
||||||
name: formattedVideoChannel.name,
|
|
||||||
displayName: formattedVideoChannel.displayName,
|
|
||||||
url: formattedVideoChannel.url,
|
|
||||||
host: formattedVideoChannel.host,
|
|
||||||
avatar: formattedVideoChannel.avatar
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options) {
|
|
||||||
if (options.additionalAttributes.state === true) {
|
|
||||||
videoObject.state = {
|
|
||||||
id: this.state,
|
|
||||||
label: VideoModel.getStateLabel(this.state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.additionalAttributes.waitTranscoding === true) {
|
|
||||||
videoObject.waitTranscoding = this.waitTranscoding
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.additionalAttributes.scheduledUpdate === true && this.ScheduleVideoUpdate) {
|
|
||||||
videoObject.scheduledUpdate = {
|
|
||||||
updateAt: this.ScheduleVideoUpdate.updateAt,
|
|
||||||
privacy: this.ScheduleVideoUpdate.privacy || undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.additionalAttributes.blacklistInfo === true) {
|
|
||||||
videoObject.blacklisted = !!this.VideoBlacklist
|
|
||||||
videoObject.blacklistedReason = this.VideoBlacklist ? this.VideoBlacklist.reason : null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return videoObject
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toFormattedDetailsJSON (): VideoDetails {
|
toFormattedDetailsJSON (): VideoDetails {
|
||||||
const formattedJson = this.toFormattedJSON({
|
return videoModelToFormattedDetailsJSON(this)
|
||||||
additionalAttributes: {
|
|
||||||
scheduledUpdate: true,
|
|
||||||
blacklistInfo: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const detailsJson = {
|
|
||||||
support: this.support,
|
|
||||||
descriptionPath: this.getDescriptionPath(),
|
|
||||||
channel: this.VideoChannel.toFormattedJSON(),
|
|
||||||
account: this.VideoChannel.Account.toFormattedJSON(),
|
|
||||||
tags: map(this.Tags, 'name'),
|
|
||||||
commentsEnabled: this.commentsEnabled,
|
|
||||||
waitTranscoding: this.waitTranscoding,
|
|
||||||
state: {
|
|
||||||
id: this.state,
|
|
||||||
label: VideoModel.getStateLabel(this.state)
|
|
||||||
},
|
|
||||||
files: []
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format and sort video files
|
|
||||||
detailsJson.files = this.getFormattedVideoFilesJSON()
|
|
||||||
|
|
||||||
return Object.assign(formattedJson, detailsJson)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getFormattedVideoFilesJSON (): VideoFile[] {
|
getFormattedVideoFilesJSON (): VideoFile[] {
|
||||||
const { baseUrlHttp, baseUrlWs } = this.getBaseUrls()
|
return videoFilesModelToFormattedJSON(this, this.VideoFiles)
|
||||||
|
|
||||||
return this.VideoFiles
|
|
||||||
.map(videoFile => {
|
|
||||||
let resolutionLabel = videoFile.resolution + 'p'
|
|
||||||
|
|
||||||
return {
|
|
||||||
resolution: {
|
|
||||||
id: videoFile.resolution,
|
|
||||||
label: resolutionLabel
|
|
||||||
},
|
|
||||||
magnetUri: this.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs),
|
|
||||||
size: videoFile.size,
|
|
||||||
fps: videoFile.fps,
|
|
||||||
torrentUrl: this.getTorrentUrl(videoFile, baseUrlHttp),
|
|
||||||
torrentDownloadUrl: this.getTorrentDownloadUrl(videoFile, baseUrlHttp),
|
|
||||||
fileUrl: this.getVideoFileUrl(videoFile, baseUrlHttp),
|
|
||||||
fileDownloadUrl: this.getVideoFileDownloadUrl(videoFile, baseUrlHttp)
|
|
||||||
} as VideoFile
|
|
||||||
})
|
|
||||||
.sort((a, b) => {
|
|
||||||
if (a.resolution.id < b.resolution.id) return 1
|
|
||||||
if (a.resolution.id === b.resolution.id) return 0
|
|
||||||
return -1
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toActivityPubObject (): VideoTorrentObject {
|
toActivityPubObject (): VideoTorrentObject {
|
||||||
const { baseUrlHttp, baseUrlWs } = this.getBaseUrls()
|
return videoModelToActivityPubObject(this)
|
||||||
if (!this.Tags) this.Tags = []
|
|
||||||
|
|
||||||
const tag = this.Tags.map(t => ({
|
|
||||||
type: 'Hashtag' as 'Hashtag',
|
|
||||||
name: t.name
|
|
||||||
}))
|
|
||||||
|
|
||||||
let language
|
|
||||||
if (this.language) {
|
|
||||||
language = {
|
|
||||||
identifier: this.language,
|
|
||||||
name: VideoModel.getLanguageLabel(this.language)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let category
|
|
||||||
if (this.category) {
|
|
||||||
category = {
|
|
||||||
identifier: this.category + '',
|
|
||||||
name: VideoModel.getCategoryLabel(this.category)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let licence
|
|
||||||
if (this.licence) {
|
|
||||||
licence = {
|
|
||||||
identifier: this.licence + '',
|
|
||||||
name: VideoModel.getLicenceLabel(this.licence)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const url: ActivityUrlObject[] = []
|
|
||||||
for (const file of this.VideoFiles) {
|
|
||||||
url.push({
|
|
||||||
type: 'Link',
|
|
||||||
mimeType: VIDEO_EXT_MIMETYPE[ file.extname ] as any,
|
|
||||||
href: this.getVideoFileUrl(file, baseUrlHttp),
|
|
||||||
height: file.resolution,
|
|
||||||
size: file.size,
|
|
||||||
fps: file.fps
|
|
||||||
})
|
|
||||||
|
|
||||||
url.push({
|
|
||||||
type: 'Link',
|
|
||||||
mimeType: 'application/x-bittorrent' as 'application/x-bittorrent',
|
|
||||||
href: this.getTorrentUrl(file, baseUrlHttp),
|
|
||||||
height: file.resolution
|
|
||||||
})
|
|
||||||
|
|
||||||
url.push({
|
|
||||||
type: 'Link',
|
|
||||||
mimeType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
|
|
||||||
href: this.generateMagnetUri(file, baseUrlHttp, baseUrlWs),
|
|
||||||
height: file.resolution
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add video url too
|
|
||||||
url.push({
|
|
||||||
type: 'Link',
|
|
||||||
mimeType: 'text/html',
|
|
||||||
href: CONFIG.WEBSERVER.URL + '/videos/watch/' + this.uuid
|
|
||||||
})
|
|
||||||
|
|
||||||
const subtitleLanguage = []
|
|
||||||
for (const caption of this.VideoCaptions) {
|
|
||||||
subtitleLanguage.push({
|
|
||||||
identifier: caption.language,
|
|
||||||
name: VideoCaptionModel.getLanguageLabel(caption.language)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'Video' as 'Video',
|
|
||||||
id: this.url,
|
|
||||||
name: this.name,
|
|
||||||
duration: this.getActivityStreamDuration(),
|
|
||||||
uuid: this.uuid,
|
|
||||||
tag,
|
|
||||||
category,
|
|
||||||
licence,
|
|
||||||
language,
|
|
||||||
views: this.views,
|
|
||||||
sensitive: this.nsfw,
|
|
||||||
waitTranscoding: this.waitTranscoding,
|
|
||||||
state: this.state,
|
|
||||||
commentsEnabled: this.commentsEnabled,
|
|
||||||
published: this.publishedAt.toISOString(),
|
|
||||||
updated: this.updatedAt.toISOString(),
|
|
||||||
mediaType: 'text/markdown',
|
|
||||||
content: this.getTruncatedDescription(),
|
|
||||||
support: this.support,
|
|
||||||
subtitleLanguage,
|
|
||||||
icon: {
|
|
||||||
type: 'Image',
|
|
||||||
url: this.getThumbnailUrl(baseUrlHttp),
|
|
||||||
mediaType: 'image/jpeg',
|
|
||||||
width: THUMBNAILS_SIZE.width,
|
|
||||||
height: THUMBNAILS_SIZE.height
|
|
||||||
},
|
|
||||||
url,
|
|
||||||
likes: getVideoLikesActivityPubUrl(this),
|
|
||||||
dislikes: getVideoDislikesActivityPubUrl(this),
|
|
||||||
shares: getVideoSharesActivityPubUrl(this),
|
|
||||||
comments: getVideoCommentsActivityPubUrl(this),
|
|
||||||
attributedTo: [
|
|
||||||
{
|
|
||||||
type: 'Person',
|
|
||||||
id: this.VideoChannel.Account.Actor.url
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'Group',
|
|
||||||
id: this.VideoChannel.Actor.url
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getTruncatedDescription () {
|
getTruncatedDescription () {
|
||||||
|
@ -1645,123 +1392,6 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
return peertubeTruncate(this.description, maxLength)
|
return peertubeTruncate(this.description, maxLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
async optimizeOriginalVideofile () {
|
|
||||||
const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
|
|
||||||
const newExtname = '.mp4'
|
|
||||||
const inputVideoFile = this.getOriginalFile()
|
|
||||||
const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
|
|
||||||
const videoTranscodedPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
|
|
||||||
|
|
||||||
const transcodeOptions = {
|
|
||||||
inputPath: videoInputPath,
|
|
||||||
outputPath: videoTranscodedPath
|
|
||||||
}
|
|
||||||
|
|
||||||
// Could be very long!
|
|
||||||
await transcode(transcodeOptions)
|
|
||||||
|
|
||||||
try {
|
|
||||||
await remove(videoInputPath)
|
|
||||||
|
|
||||||
// Important to do this before getVideoFilename() to take in account the new file extension
|
|
||||||
inputVideoFile.set('extname', newExtname)
|
|
||||||
|
|
||||||
const videoOutputPath = this.getVideoFilePath(inputVideoFile)
|
|
||||||
await rename(videoTranscodedPath, videoOutputPath)
|
|
||||||
const stats = await stat(videoOutputPath)
|
|
||||||
const fps = await getVideoFileFPS(videoOutputPath)
|
|
||||||
|
|
||||||
inputVideoFile.set('size', stats.size)
|
|
||||||
inputVideoFile.set('fps', fps)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async transcodeOriginalVideofile (resolution: VideoResolution, isPortraitMode: boolean) {
|
|
||||||
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 = new VideoFileModel({
|
|
||||||
resolution,
|
|
||||||
extname,
|
|
||||||
size: 0,
|
|
||||||
videoId: this.id
|
|
||||||
})
|
|
||||||
const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile))
|
|
||||||
|
|
||||||
const transcodeOptions = {
|
|
||||||
inputPath: videoInputPath,
|
|
||||||
outputPath: videoOutputPath,
|
|
||||||
resolution,
|
|
||||||
isPortraitMode
|
|
||||||
}
|
|
||||||
|
|
||||||
await transcode(transcodeOptions)
|
|
||||||
|
|
||||||
const stats = await stat(videoOutputPath)
|
|
||||||
const fps = await getVideoFileFPS(videoOutputPath)
|
|
||||||
|
|
||||||
newVideoFile.set('size', stats.size)
|
|
||||||
newVideoFile.set('fps', fps)
|
|
||||||
|
|
||||||
await this.createTorrentAndSetInfoHash(newVideoFile)
|
|
||||||
|
|
||||||
await newVideoFile.save()
|
|
||||||
|
|
||||||
this.VideoFiles.push(newVideoFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
async importVideoFile (inputFilePath: string) {
|
|
||||||
const { videoFileResolution } = await getVideoFileResolution(inputFilePath)
|
|
||||||
const { size } = await stat(inputFilePath)
|
|
||||||
const fps = await getVideoFileFPS(inputFilePath)
|
|
||||||
|
|
||||||
let updatedVideoFile = new VideoFileModel({
|
|
||||||
resolution: videoFileResolution,
|
|
||||||
extname: extname(inputFilePath),
|
|
||||||
size,
|
|
||||||
fps,
|
|
||||||
videoId: this.id
|
|
||||||
})
|
|
||||||
|
|
||||||
const currentVideoFile = this.VideoFiles.find(videoFile => videoFile.resolution === updatedVideoFile.resolution)
|
|
||||||
|
|
||||||
if (currentVideoFile) {
|
|
||||||
// Remove old file and old torrent
|
|
||||||
await this.removeFile(currentVideoFile)
|
|
||||||
await this.removeTorrent(currentVideoFile)
|
|
||||||
// Remove the old video file from the array
|
|
||||||
this.VideoFiles = this.VideoFiles.filter(f => f !== currentVideoFile)
|
|
||||||
|
|
||||||
// Update the database
|
|
||||||
currentVideoFile.set('extname', updatedVideoFile.extname)
|
|
||||||
currentVideoFile.set('size', updatedVideoFile.size)
|
|
||||||
currentVideoFile.set('fps', updatedVideoFile.fps)
|
|
||||||
|
|
||||||
updatedVideoFile = currentVideoFile
|
|
||||||
}
|
|
||||||
|
|
||||||
const outputPath = this.getVideoFilePath(updatedVideoFile)
|
|
||||||
await copy(inputFilePath, outputPath)
|
|
||||||
|
|
||||||
await this.createTorrentAndSetInfoHash(updatedVideoFile)
|
|
||||||
|
|
||||||
await updatedVideoFile.save()
|
|
||||||
|
|
||||||
this.VideoFiles.push(updatedVideoFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
getOriginalFileResolution () {
|
getOriginalFileResolution () {
|
||||||
const originalFilePath = this.getVideoFilePath(this.getOriginalFile())
|
const originalFilePath = this.getVideoFilePath(this.getOriginalFile())
|
||||||
|
|
||||||
|
@ -1796,11 +1426,6 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
.catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
|
.catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
|
||||||
}
|
}
|
||||||
|
|
||||||
getActivityStreamDuration () {
|
|
||||||
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
|
|
||||||
return 'PT' + this.duration + 'S'
|
|
||||||
}
|
|
||||||
|
|
||||||
isOutdated () {
|
isOutdated () {
|
||||||
if (this.isOwned()) return false
|
if (this.isOwned()) return false
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue