From 098eb37797fdadd4adf660b76867da68061fd588 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 18 Sep 2018 11:02:51 +0200 Subject: [PATCH] Reduce video.ts file size by moving some methods in other files --- config/test.yaml | 2 +- server/lib/client-html.ts | 3 +- server/lib/job-queue/handlers/video-file.ts | 7 +- server/lib/video-transcoding.ts | 130 ++++++ server/models/redundancy/video-redundancy.ts | 2 +- server/models/video/video-format-utils.ts | 295 +++++++++++++ server/models/video/video.ts | 419 +------------------ 7 files changed, 455 insertions(+), 403 deletions(-) create mode 100644 server/lib/video-transcoding.ts create mode 100644 server/models/video/video-format-utils.ts diff --git a/config/test.yaml b/config/test.yaml index 16113211e..d3e0e49ac 100644 --- a/config/test.yaml +++ b/config/test.yaml @@ -32,7 +32,7 @@ redundancy: - size: '10MB' strategy: 'recently-added' - minViews: 10 + minViews: 1 cache: previews: diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts index a69e09c32..b1088c096 100644 --- a/server/lib/client-html.ts +++ b/server/lib/client-html.ts @@ -8,6 +8,7 @@ import { VideoModel } from '../models/video/video' import * as validator from 'validator' import { VideoPrivacy } from '../../shared/models/videos' import { readFile } from 'fs-extra' +import { getActivityStreamDuration } from '../models/video/video-format-utils' export class ClientHtml { @@ -150,7 +151,7 @@ export class ClientHtml { description: videoDescriptionEscaped, thumbnailUrl: previewUrl, uploadDate: video.createdAt.toISOString(), - duration: video.getActivityStreamDuration(), + duration: getActivityStreamDuration(video.duration), contentUrl: videoUrl, embedUrl: embedUrl, interactionCount: video.views diff --git a/server/lib/job-queue/handlers/video-file.ts b/server/lib/job-queue/handlers/video-file.ts index c6308f7a6..2c9ca8e12 100644 --- a/server/lib/job-queue/handlers/video-file.ts +++ b/server/lib/job-queue/handlers/video-file.ts @@ -8,6 +8,7 @@ import { retryTransactionWrapper } from '../../../helpers/database-utils' import { sequelizeTypescript } from '../../../initializers' import * as Bluebird from 'bluebird' import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' +import { importVideoFile, transcodeOriginalVideofile, optimizeOriginalVideofile } from '../../video-transcoding' export type VideoFilePayload = { videoUUID: string @@ -32,7 +33,7 @@ async function processVideoFileImport (job: Bull.Job) { return undefined } - await video.importVideoFile(payload.filePath) + await importVideoFile(video, payload.filePath) await onVideoFileTranscoderOrImportSuccess(video) return video @@ -51,11 +52,11 @@ async function processVideoFile (job: Bull.Job) { // Transcoding in other resolution if (payload.resolution) { - await video.transcodeOriginalVideofile(payload.resolution, payload.isPortraitMode || false) + await transcodeOriginalVideofile(video, payload.resolution, payload.isPortraitMode || false) await retryTransactionWrapper(onVideoFileTranscoderOrImportSuccess, video) } else { - await video.optimizeOriginalVideofile() + await optimizeOriginalVideofile(video) await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload.isNewVideo) } diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts new file mode 100644 index 000000000..bf3ff78c2 --- /dev/null +++ b/server/lib/video-transcoding.ts @@ -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 +} diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index 6ae02efb9..fb07287a8 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts @@ -193,7 +193,7 @@ export class VideoRedundancyModel extends Model { // On VideoModel! const query = { attributes: [ 'id', 'publishedAt' ], - // logging: !isTestInstance(), + logging: !isTestInstance(), limit: randomizedFactor, order: getVideoSort('-publishedAt'), where: { diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts new file mode 100644 index 000000000..fae38507b --- /dev/null +++ b/server/models/video/video-format-utils.ts @@ -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 +} diff --git a/server/models/video/video.ts b/server/models/video/video.ts index b7d3f184f..ce856aed2 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -1,8 +1,8 @@ import * as Bluebird from 'bluebird' -import { map, maxBy } from 'lodash' +import { maxBy } from 'lodash' import * as magnetUtil from 'magnet-uri' import * as parseTorrent from 'parse-torrent' -import { extname, join } from 'path' +import { join } from 'path' import * as Sequelize from 'sequelize' import { AllowNull, @@ -27,7 +27,7 @@ import { Table, UpdatedAt } from 'sequelize-typescript' -import { ActivityUrlObject, VideoPrivacy, VideoResolution, VideoState } from '../../../shared' +import { VideoPrivacy, VideoState } from '../../../shared' import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' import { VideoFilter } from '../../../shared/models/videos/video-query.type' @@ -45,7 +45,7 @@ import { isVideoStateValid, isVideoSupportValid } 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 { getServerActor } from '../../helpers/utils' import { @@ -59,18 +59,11 @@ import { STATIC_PATHS, THUMBNAILS_SIZE, VIDEO_CATEGORIES, - VIDEO_EXT_MIMETYPE, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES, VIDEO_STATES } from '../../initializers' -import { - getVideoCommentsActivityPubUrl, - getVideoDislikesActivityPubUrl, - getVideoLikesActivityPubUrl, - getVideoSharesActivityPubUrl -} from '../../lib/activitypub' import { sendDeleteVideo } from '../../lib/activitypub/send' import { AccountModel } from '../account/account' import { AccountVideoRateModel } from '../account/account-video-rate' @@ -88,9 +81,16 @@ import { VideoTagModel } from './video-tag' import { ScheduleVideoUpdateModel } from './schedule-video-update' import { VideoCaptionModel } from './video-caption' 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 { 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 const indexes: Sequelize.DefineIndexesOptions[] = [ @@ -1257,23 +1257,23 @@ export class VideoModel extends Model { } } - private static getCategoryLabel (id: number) { + static getCategoryLabel (id: number) { return VIDEO_CATEGORIES[ id ] || 'Misc' } - private static getLicenceLabel (id: number) { + static getLicenceLabel (id: number) { return VIDEO_LICENCES[ id ] || 'Unknown' } - private static getLanguageLabel (id: string) { + static getLanguageLabel (id: string) { return VIDEO_LANGUAGES[ id ] || 'Unknown' } - private static getPrivacyLabel (id: number) { + static getPrivacyLabel (id: number) { return VIDEO_PRIVACIES[ id ] || 'Unknown' } - private static getStateLabel (id: number) { + static getStateLabel (id: number) { return VIDEO_STATES[ id ] || 'Unknown' } @@ -1369,273 +1369,20 @@ export class VideoModel extends Model { return join(STATIC_PATHS.PREVIEWS, this.getPreviewName()) } - toFormattedJSON (options?: { - additionalAttributes: { - 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 + toFormattedJSON (options?: VideoFormattingJSONOptions): Video { + return videoModelToFormattedJSON(this, options) } toFormattedDetailsJSON (): VideoDetails { - const formattedJson = this.toFormattedJSON({ - 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) + return videoModelToFormattedDetailsJSON(this) } getFormattedVideoFilesJSON (): VideoFile[] { - const { baseUrlHttp, baseUrlWs } = this.getBaseUrls() - - 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 - }) + return videoFilesModelToFormattedJSON(this, this.VideoFiles) } toActivityPubObject (): VideoTorrentObject { - const { baseUrlHttp, baseUrlWs } = this.getBaseUrls() - 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 - } - ] - } + return videoModelToActivityPubObject(this) } getTruncatedDescription () { @@ -1645,123 +1392,6 @@ export class VideoModel extends Model { 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 () { const originalFilePath = this.getVideoFilePath(this.getOriginalFile()) @@ -1796,11 +1426,6 @@ export class VideoModel extends Model { .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 () { if (this.isOwned()) return false