From 3545e72c686ff1725bbdfd8d16d693e2f4aa75a3 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 12 Oct 2022 16:09:02 +0200 Subject: Put private videos under a specific subdirectory --- server/lib/auth/oauth.ts | 9 +- .../lib/job-queue/handlers/manage-video-torrent.ts | 2 +- .../job-queue/handlers/move-to-object-storage.ts | 6 +- server/lib/job-queue/handlers/video-live-ending.ts | 22 +- server/lib/job-queue/handlers/video-transcoding.ts | 95 +++--- server/lib/object-storage/videos.ts | 9 +- server/lib/paths.ts | 17 +- server/lib/schedulers/update-videos-scheduler.ts | 66 ++-- .../lib/schedulers/videos-redundancy-scheduler.ts | 4 +- server/lib/transcoding/transcoding.ts | 367 ++++++++++++--------- server/lib/video-path-manager.ts | 51 ++- server/lib/video-privacy.ts | 96 ++++++ server/lib/video-tokens-manager.ts | 49 +++ server/lib/video.ts | 61 +++- 14 files changed, 601 insertions(+), 253 deletions(-) create mode 100644 server/lib/video-privacy.ts create mode 100644 server/lib/video-tokens-manager.ts (limited to 'server/lib') diff --git a/server/lib/auth/oauth.ts b/server/lib/auth/oauth.ts index 35b05ec5a..bc0d4301f 100644 --- a/server/lib/auth/oauth.ts +++ b/server/lib/auth/oauth.ts @@ -95,14 +95,9 @@ async function handleOAuthToken (req: express.Request, options: { refreshTokenAu function handleOAuthAuthenticate ( req: express.Request, - res: express.Response, - authenticateInQuery = false + res: express.Response ) { - const options = authenticateInQuery - ? { allowBearerTokensInQueryString: true } - : {} - - return oAuthServer.authenticate(new Request(req), new Response(res), options) + return oAuthServer.authenticate(new Request(req), new Response(res)) } export { diff --git a/server/lib/job-queue/handlers/manage-video-torrent.ts b/server/lib/job-queue/handlers/manage-video-torrent.ts index 03aa414c9..425915c96 100644 --- a/server/lib/job-queue/handlers/manage-video-torrent.ts +++ b/server/lib/job-queue/handlers/manage-video-torrent.ts @@ -82,7 +82,7 @@ async function loadStreamingPlaylistOrLog (streamingPlaylistId: number) { async function loadFileOrLog (videoFileId: number) { if (!videoFileId) return undefined - const file = await VideoFileModel.loadWithVideo(videoFileId) + const file = await VideoFileModel.load(videoFileId) if (!file) { logger.debug('Do not process torrent for file %d: does not exist anymore.', videoFileId) diff --git a/server/lib/job-queue/handlers/move-to-object-storage.ts b/server/lib/job-queue/handlers/move-to-object-storage.ts index 28c3d325d..0b68555d1 100644 --- a/server/lib/job-queue/handlers/move-to-object-storage.ts +++ b/server/lib/job-queue/handlers/move-to-object-storage.ts @@ -3,10 +3,10 @@ import { remove } from 'fs-extra' import { join } from 'path' import { logger, loggerTagsFactory } from '@server/helpers/logger' import { updateTorrentMetadata } from '@server/helpers/webtorrent' -import { CONFIG } from '@server/initializers/config' import { P2P_MEDIA_LOADER_PEER_VERSION } from '@server/initializers/constants' import { storeHLSFileFromFilename, storeWebTorrentFile } from '@server/lib/object-storage' import { getHLSDirectory, getHlsResolutionPlaylistFilename } from '@server/lib/paths' +import { VideoPathManager } from '@server/lib/video-path-manager' import { moveToFailedMoveToObjectStorageState, moveToNextState } from '@server/lib/video-state' import { VideoModel } from '@server/models/video/video' import { VideoJobInfoModel } from '@server/models/video/video-job-info' @@ -72,9 +72,9 @@ async function moveWebTorrentFiles (video: MVideoWithAllFiles) { for (const file of video.VideoFiles) { if (file.storage !== VideoStorage.FILE_SYSTEM) continue - const fileUrl = await storeWebTorrentFile(file.filename) + const fileUrl = await storeWebTorrentFile(video, file) - const oldPath = join(CONFIG.STORAGE.VIDEOS_DIR, file.filename) + const oldPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, file) await onFileMoved({ videoOrPlaylist: video, file, fileUrl, oldPath }) } } diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts index 7dbffc955..c6263f55a 100644 --- a/server/lib/job-queue/handlers/video-live-ending.ts +++ b/server/lib/job-queue/handlers/video-live-ending.ts @@ -18,6 +18,7 @@ import { VideoStreamingPlaylistModel } from '@server/models/video/video-streamin import { MVideo, MVideoLive, MVideoLiveSession, MVideoWithAllFiles } from '@server/types/models' import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' import { logger, loggerTagsFactory } from '../../../helpers/logger' +import { VideoPathManager } from '@server/lib/video-path-manager' const lTags = loggerTagsFactory('live', 'job') @@ -205,18 +206,27 @@ async function assignReplayFilesToVideo (options: { const concatenatedTsFiles = await readdir(replayDirectory) for (const concatenatedTsFile of concatenatedTsFiles) { + const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) + const concatenatedTsFilePath = join(replayDirectory, concatenatedTsFile) const probe = await ffprobePromise(concatenatedTsFilePath) const { audioStream } = await getAudioStream(concatenatedTsFilePath, probe) const { resolution } = await getVideoStreamDimensionsInfo(concatenatedTsFilePath, probe) - await generateHlsPlaylistResolutionFromTS({ - video, - concatenatedTsFilePath, - resolution, - isAAC: audioStream?.codec_name === 'aac' - }) + try { + await generateHlsPlaylistResolutionFromTS({ + video, + inputFileMutexReleaser, + concatenatedTsFilePath, + resolution, + isAAC: audioStream?.codec_name === 'aac' + }) + } catch (err) { + logger.error('Cannot generate HLS playlist resolution from TS files.', { err }) + } + + inputFileMutexReleaser() } return video diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts index b0e92acf7..48c675678 100644 --- a/server/lib/job-queue/handlers/video-transcoding.ts +++ b/server/lib/job-queue/handlers/video-transcoding.ts @@ -94,15 +94,24 @@ async function handleHLSJob (job: Job, payload: HLSTranscodingPayload, video: MV const videoOrStreamingPlaylist = videoFileInput.getVideoOrStreamingPlaylist() - await VideoPathManager.Instance.makeAvailableVideoFile(videoFileInput.withVideoOrPlaylist(videoOrStreamingPlaylist), videoInputPath => { - return generateHlsPlaylistResolution({ - video, - videoInputPath, - resolution: payload.resolution, - copyCodecs: payload.copyCodecs, - job + const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) + + try { + await videoFileInput.getVideo().reload() + + await VideoPathManager.Instance.makeAvailableVideoFile(videoFileInput.withVideoOrPlaylist(videoOrStreamingPlaylist), videoInputPath => { + return generateHlsPlaylistResolution({ + video, + videoInputPath, + inputFileMutexReleaser, + resolution: payload.resolution, + copyCodecs: payload.copyCodecs, + job + }) }) - }) + } finally { + inputFileMutexReleaser() + } logger.info('HLS transcoding job for %s ended.', video.uuid, lTags(video.uuid)) @@ -177,38 +186,44 @@ async function onVideoFirstWebTorrentTranscoding ( transcodeType: TranscodeVODOptionsType, user: MUserId ) { - const { resolution, audioStream } = await videoArg.probeMaxQualityFile() - - // Maybe the video changed in database, refresh it - const videoDatabase = await VideoModel.loadFull(videoArg.uuid) - // Video does not exist anymore - if (!videoDatabase) return undefined - - // Generate HLS version of the original file - const originalFileHLSPayload = { - ...payload, - - hasAudio: !!audioStream, - resolution: videoDatabase.getMaxQualityFile().resolution, - // If we quick transcoded original file, force transcoding for HLS to avoid some weird playback issues - copyCodecs: transcodeType !== 'quick-transcode', - isMaxQuality: true - } - const hasHls = await createHlsJobIfEnabled(user, originalFileHLSPayload) - const hasNewResolutions = await createLowerResolutionsJobs({ - video: videoDatabase, - user, - videoFileResolution: resolution, - hasAudio: !!audioStream, - type: 'webtorrent', - isNewVideo: payload.isNewVideo ?? true - }) - - await VideoJobInfoModel.decrease(videoDatabase.uuid, 'pendingTranscode') - - // Move to next state if there are no other resolutions to generate - if (!hasHls && !hasNewResolutions) { - await retryTransactionWrapper(moveToNextState, { video: videoDatabase, isNewVideo: payload.isNewVideo }) + const mutexReleaser = await VideoPathManager.Instance.lockFiles(videoArg.uuid) + + try { + // Maybe the video changed in database, refresh it + const videoDatabase = await VideoModel.loadFull(videoArg.uuid) + // Video does not exist anymore + if (!videoDatabase) return undefined + + const { resolution, audioStream } = await videoDatabase.probeMaxQualityFile() + + // Generate HLS version of the original file + const originalFileHLSPayload = { + ...payload, + + hasAudio: !!audioStream, + resolution: videoDatabase.getMaxQualityFile().resolution, + // If we quick transcoded original file, force transcoding for HLS to avoid some weird playback issues + copyCodecs: transcodeType !== 'quick-transcode', + isMaxQuality: true + } + const hasHls = await createHlsJobIfEnabled(user, originalFileHLSPayload) + const hasNewResolutions = await createLowerResolutionsJobs({ + video: videoDatabase, + user, + videoFileResolution: resolution, + hasAudio: !!audioStream, + type: 'webtorrent', + isNewVideo: payload.isNewVideo ?? true + }) + + await VideoJobInfoModel.decrease(videoDatabase.uuid, 'pendingTranscode') + + // Move to next state if there are no other resolutions to generate + if (!hasHls && !hasNewResolutions) { + await retryTransactionWrapper(moveToNextState, { video: videoDatabase, isNewVideo: payload.isNewVideo }) + } + } finally { + mutexReleaser() } } diff --git a/server/lib/object-storage/videos.ts b/server/lib/object-storage/videos.ts index 62aae248b..e323baaa2 100644 --- a/server/lib/object-storage/videos.ts +++ b/server/lib/object-storage/videos.ts @@ -1,8 +1,9 @@ import { basename, join } from 'path' import { logger } from '@server/helpers/logger' import { CONFIG } from '@server/initializers/config' -import { MStreamingPlaylistVideo, MVideoFile } from '@server/types/models' +import { MStreamingPlaylistVideo, MVideo, MVideoFile } from '@server/types/models' import { getHLSDirectory } from '../paths' +import { VideoPathManager } from '../video-path-manager' import { generateHLSObjectBaseStorageKey, generateHLSObjectStorageKey, generateWebTorrentObjectStorageKey } from './keys' import { listKeysOfPrefix, lTags, makeAvailable, removeObject, removePrefix, storeObject } from './shared' @@ -30,10 +31,10 @@ function storeHLSFileFromPath (playlist: MStreamingPlaylistVideo, path: string) // --------------------------------------------------------------------------- -function storeWebTorrentFile (filename: string) { +function storeWebTorrentFile (video: MVideo, file: MVideoFile) { return storeObject({ - inputPath: join(CONFIG.STORAGE.VIDEOS_DIR, filename), - objectStorageKey: generateWebTorrentObjectStorageKey(filename), + inputPath: VideoPathManager.Instance.getFSVideoFileOutputPath(video, file), + objectStorageKey: generateWebTorrentObjectStorageKey(file.filename), bucketInfo: CONFIG.OBJECT_STORAGE.VIDEOS }) } diff --git a/server/lib/paths.ts b/server/lib/paths.ts index b29854700..470970f55 100644 --- a/server/lib/paths.ts +++ b/server/lib/paths.ts @@ -1,9 +1,10 @@ import { join } from 'path' import { CONFIG } from '@server/initializers/config' -import { HLS_REDUNDANCY_DIRECTORY, HLS_STREAMING_PLAYLIST_DIRECTORY, VIDEO_LIVE } from '@server/initializers/constants' +import { DIRECTORIES, VIDEO_LIVE } from '@server/initializers/constants' import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoUUID } from '@server/types/models' import { removeFragmentedMP4Ext } from '@shared/core-utils' import { buildUUID } from '@shared/extra-utils' +import { isVideoInPrivateDirectory } from './video-privacy' // ################## Video file name ################## @@ -17,20 +18,24 @@ function generateHLSVideoFilename (resolution: number) { // ################## Streaming playlist ################## -function getLiveDirectory (video: MVideoUUID) { +function getLiveDirectory (video: MVideo) { return getHLSDirectory(video) } -function getLiveReplayBaseDirectory (video: MVideoUUID) { +function getLiveReplayBaseDirectory (video: MVideo) { return join(getLiveDirectory(video), VIDEO_LIVE.REPLAY_DIRECTORY) } -function getHLSDirectory (video: MVideoUUID) { - return join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid) +function getHLSDirectory (video: MVideo) { + if (isVideoInPrivateDirectory(video.privacy)) { + return join(DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE, video.uuid) + } + + return join(DIRECTORIES.HLS_STREAMING_PLAYLIST.PUBLIC, video.uuid) } function getHLSRedundancyDirectory (video: MVideoUUID) { - return join(HLS_REDUNDANCY_DIRECTORY, video.uuid) + return join(DIRECTORIES.HLS_REDUNDANCY, video.uuid) } function getHlsResolutionPlaylistFilename (videoFilename: string) { diff --git a/server/lib/schedulers/update-videos-scheduler.ts b/server/lib/schedulers/update-videos-scheduler.ts index 5bfbc3cd2..30bf189db 100644 --- a/server/lib/schedulers/update-videos-scheduler.ts +++ b/server/lib/schedulers/update-videos-scheduler.ts @@ -1,11 +1,14 @@ import { VideoModel } from '@server/models/video/video' -import { MVideoFullLight } from '@server/types/models' +import { MScheduleVideoUpdate } from '@server/types/models' +import { VideoPrivacy, VideoState } from '@shared/models' import { logger } from '../../helpers/logger' import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants' import { sequelizeTypescript } from '../../initializers/database' import { ScheduleVideoUpdateModel } from '../../models/video/schedule-video-update' -import { federateVideoIfNeeded } from '../activitypub/videos' import { Notifier } from '../notifier' +import { addVideoJobsAfterUpdate } from '../video' +import { VideoPathManager } from '../video-path-manager' +import { setVideoPrivacy } from '../video-privacy' import { AbstractScheduler } from './abstract-scheduler' export class UpdateVideosScheduler extends AbstractScheduler { @@ -26,35 +29,54 @@ export class UpdateVideosScheduler extends AbstractScheduler { if (!await ScheduleVideoUpdateModel.areVideosToUpdate()) return undefined const schedules = await ScheduleVideoUpdateModel.listVideosToUpdate() - const publishedVideos: MVideoFullLight[] = [] for (const schedule of schedules) { - await sequelizeTypescript.transaction(async t => { - const video = await VideoModel.loadFull(schedule.videoId, t) + const videoOnly = await VideoModel.load(schedule.videoId) + const mutexReleaser = await VideoPathManager.Instance.lockFiles(videoOnly.uuid) - logger.info('Executing scheduled video update on %s.', video.uuid) + try { + const { video, published } = await this.updateAVideo(schedule) - if (schedule.privacy) { - const wasConfidentialVideo = video.isConfidential() - const isNewVideo = video.isNewVideo(schedule.privacy) + if (published) Notifier.Instance.notifyOnVideoPublishedAfterScheduledUpdate(video) + } catch (err) { + logger.error('Cannot update video', { err }) + } - video.setPrivacy(schedule.privacy) - await video.save({ transaction: t }) - await federateVideoIfNeeded(video, isNewVideo, t) + mutexReleaser() + } + } + + private async updateAVideo (schedule: MScheduleVideoUpdate) { + let oldPrivacy: VideoPrivacy + let isNewVideo: boolean + let published = false + + const video = await sequelizeTypescript.transaction(async t => { + const video = await VideoModel.loadFull(schedule.videoId, t) + if (video.state === VideoState.TO_TRANSCODE) return + + logger.info('Executing scheduled video update on %s.', video.uuid) + + if (schedule.privacy) { + isNewVideo = video.isNewVideo(schedule.privacy) + oldPrivacy = video.privacy - if (wasConfidentialVideo) { - publishedVideos.push(video) - } + setVideoPrivacy(video, schedule.privacy) + await video.save({ transaction: t }) + + if (oldPrivacy === VideoPrivacy.PRIVATE) { + published = true } + } - await schedule.destroy({ transaction: t }) - }) - } + await schedule.destroy({ transaction: t }) - for (const v of publishedVideos) { - Notifier.Instance.notifyOnNewVideoIfNeeded(v) - Notifier.Instance.notifyOnVideoPublishedAfterScheduledUpdate(v) - } + return video + }) + + await addVideoJobsAfterUpdate({ video, oldPrivacy, isNewVideo, nameChanged: false }) + + return { video, published } } static get Instance () { diff --git a/server/lib/schedulers/videos-redundancy-scheduler.ts b/server/lib/schedulers/videos-redundancy-scheduler.ts index 91c217615..78245fa6a 100644 --- a/server/lib/schedulers/videos-redundancy-scheduler.ts +++ b/server/lib/schedulers/videos-redundancy-scheduler.ts @@ -16,7 +16,7 @@ import { VideosRedundancyStrategy } from '../../../shared/models/redundancy' import { logger, loggerTagsFactory } from '../../helpers/logger' import { downloadWebTorrentVideo } from '../../helpers/webtorrent' import { CONFIG } from '../../initializers/config' -import { HLS_REDUNDANCY_DIRECTORY, REDUNDANCY, VIDEO_IMPORT_TIMEOUT } from '../../initializers/constants' +import { DIRECTORIES, REDUNDANCY, VIDEO_IMPORT_TIMEOUT } from '../../initializers/constants' import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' import { sendCreateCacheFile, sendUpdateCacheFile } from '../activitypub/send' import { getLocalVideoCacheFileActivityPubUrl, getLocalVideoCacheStreamingPlaylistActivityPubUrl } from '../activitypub/url' @@ -262,7 +262,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler { logger.info('Duplicating %s streaming playlist in videos redundancy with "%s" strategy.', video.url, strategy, lTags(video.uuid)) - const destDirectory = join(HLS_REDUNDANCY_DIRECTORY, video.uuid) + const destDirectory = join(DIRECTORIES.HLS_REDUNDANCY, video.uuid) const masterPlaylistUrl = playlist.getMasterPlaylistUrl(video) const maxSizeKB = this.getTotalFileSizes([], [ playlist ]) / 1000 diff --git a/server/lib/transcoding/transcoding.ts b/server/lib/transcoding/transcoding.ts index 44e26754d..736e96e65 100644 --- a/server/lib/transcoding/transcoding.ts +++ b/server/lib/transcoding/transcoding.ts @@ -1,3 +1,4 @@ +import { MutexInterface } from 'async-mutex' import { Job } from 'bullmq' import { copyFile, ensureDir, move, remove, stat } from 'fs-extra' import { basename, extname as extnameUtil, join } from 'path' @@ -6,11 +7,13 @@ import { retryTransactionWrapper } from '@server/helpers/database-utils' import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' import { sequelizeTypescript } from '@server/initializers/database' import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' +import { pick } from '@shared/core-utils' import { VideoResolution, VideoStorage } from '../../../shared/models/videos' import { buildFileMetadata, canDoQuickTranscode, computeResolutionsToTranscode, + ffprobePromise, getVideoStreamDuration, getVideoStreamFPS, transcodeVOD, @@ -33,7 +36,7 @@ import { VideoTranscodingProfilesManager } from './default-transcoding-profiles' */ // Optimize the original video file and replace it. The resolution is not changed. -function optimizeOriginalVideofile (options: { +async function optimizeOriginalVideofile (options: { video: MVideoFullLight inputVideoFile: MVideoFile job: Job @@ -43,49 +46,61 @@ function optimizeOriginalVideofile (options: { const transcodeDirectory = CONFIG.STORAGE.TMP_DIR const newExtname = '.mp4' - return VideoPathManager.Instance.makeAvailableVideoFile(inputVideoFile.withVideoOrPlaylist(video), async videoInputPath => { - const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) + const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) - const transcodeType: TranscodeVODOptionsType = await canDoQuickTranscode(videoInputPath) - ? 'quick-transcode' - : 'video' + try { + await video.reload() - const resolution = buildOriginalFileResolution(inputVideoFile.resolution) + const fileWithVideoOrPlaylist = inputVideoFile.withVideoOrPlaylist(video) - const transcodeOptions: TranscodeVODOptions = { - type: transcodeType, + const result = await VideoPathManager.Instance.makeAvailableVideoFile(fileWithVideoOrPlaylist, async videoInputPath => { + const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) - inputPath: videoInputPath, - outputPath: videoTranscodedPath, + const transcodeType: TranscodeVODOptionsType = await canDoQuickTranscode(videoInputPath) + ? 'quick-transcode' + : 'video' - availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), - profile: CONFIG.TRANSCODING.PROFILE, + const resolution = buildOriginalFileResolution(inputVideoFile.resolution) - resolution, + const transcodeOptions: TranscodeVODOptions = { + type: transcodeType, - job - } + inputPath: videoInputPath, + outputPath: videoTranscodedPath, - // Could be very long! - await transcodeVOD(transcodeOptions) + inputFileMutexReleaser, - // Important to do this before getVideoFilename() to take in account the new filename - inputVideoFile.resolution = resolution - inputVideoFile.extname = newExtname - inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname) - inputVideoFile.storage = VideoStorage.FILE_SYSTEM + availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), + profile: CONFIG.TRANSCODING.PROFILE, - const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, inputVideoFile) + resolution, - const { videoFile } = await onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) - await remove(videoInputPath) + job + } - return { transcodeType, videoFile } - }) + // Could be very long! + await transcodeVOD(transcodeOptions) + + // Important to do this before getVideoFilename() to take in account the new filename + inputVideoFile.resolution = resolution + inputVideoFile.extname = newExtname + inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname) + inputVideoFile.storage = VideoStorage.FILE_SYSTEM + + const { videoFile } = await onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, inputVideoFile) + await remove(videoInputPath) + + return { transcodeType, videoFile } + }) + + return result + } finally { + inputFileMutexReleaser() + } } // Transcode the original video file to a lower resolution compatible with WebTorrent -function transcodeNewWebTorrentResolution (options: { +async function transcodeNewWebTorrentResolution (options: { video: MVideoFullLight resolution: VideoResolution job: Job @@ -95,53 +110,68 @@ function transcodeNewWebTorrentResolution (options: { const transcodeDirectory = CONFIG.STORAGE.TMP_DIR const newExtname = '.mp4' - return VideoPathManager.Instance.makeAvailableVideoFile(video.getMaxQualityFile().withVideoOrPlaylist(video), async videoInputPath => { - const newVideoFile = new VideoFileModel({ - resolution, - extname: newExtname, - filename: generateWebTorrentVideoFilename(resolution, newExtname), - size: 0, - videoId: video.id - }) + const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) - const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newVideoFile) - const videoTranscodedPath = join(transcodeDirectory, newVideoFile.filename) + try { + await video.reload() - const transcodeOptions = resolution === VideoResolution.H_NOVIDEO - ? { - type: 'only-audio' as 'only-audio', + const file = video.getMaxQualityFile().withVideoOrPlaylist(video) - inputPath: videoInputPath, - outputPath: videoTranscodedPath, + const result = await VideoPathManager.Instance.makeAvailableVideoFile(file, async videoInputPath => { + const newVideoFile = new VideoFileModel({ + resolution, + extname: newExtname, + filename: generateWebTorrentVideoFilename(resolution, newExtname), + size: 0, + videoId: video.id + }) - availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), - profile: CONFIG.TRANSCODING.PROFILE, + const videoTranscodedPath = join(transcodeDirectory, newVideoFile.filename) - resolution, + const transcodeOptions = resolution === VideoResolution.H_NOVIDEO + ? { + type: 'only-audio' as 'only-audio', - job - } - : { - type: 'video' as 'video', - inputPath: videoInputPath, - outputPath: videoTranscodedPath, + inputPath: videoInputPath, + outputPath: videoTranscodedPath, - availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), - profile: CONFIG.TRANSCODING.PROFILE, + inputFileMutexReleaser, - resolution, + availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), + profile: CONFIG.TRANSCODING.PROFILE, - job - } + resolution, - await transcodeVOD(transcodeOptions) + job + } + : { + type: 'video' as 'video', + inputPath: videoInputPath, + outputPath: videoTranscodedPath, - return onWebTorrentVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath) - }) + inputFileMutexReleaser, + + availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), + profile: CONFIG.TRANSCODING.PROFILE, + + resolution, + + job + } + + await transcodeVOD(transcodeOptions) + + return onWebTorrentVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, newVideoFile) + }) + + return result + } finally { + inputFileMutexReleaser() + } } // Merge an image with an audio file to create a video -function mergeAudioVideofile (options: { +async function mergeAudioVideofile (options: { video: MVideoFullLight resolution: VideoResolution job: Job @@ -151,54 +181,67 @@ function mergeAudioVideofile (options: { const transcodeDirectory = CONFIG.STORAGE.TMP_DIR const newExtname = '.mp4' - const inputVideoFile = video.getMinQualityFile() + const inputFileMutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) - return VideoPathManager.Instance.makeAvailableVideoFile(inputVideoFile.withVideoOrPlaylist(video), async audioInputPath => { - const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) + try { + await video.reload() - // If the user updates the video preview during transcoding - const previewPath = video.getPreview().getPath() - const tmpPreviewPath = join(CONFIG.STORAGE.TMP_DIR, basename(previewPath)) - await copyFile(previewPath, tmpPreviewPath) + const inputVideoFile = video.getMinQualityFile() - const transcodeOptions = { - type: 'merge-audio' as 'merge-audio', + const fileWithVideoOrPlaylist = inputVideoFile.withVideoOrPlaylist(video) - inputPath: tmpPreviewPath, - outputPath: videoTranscodedPath, + const result = await VideoPathManager.Instance.makeAvailableVideoFile(fileWithVideoOrPlaylist, async audioInputPath => { + const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) - availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), - profile: CONFIG.TRANSCODING.PROFILE, + // If the user updates the video preview during transcoding + const previewPath = video.getPreview().getPath() + const tmpPreviewPath = join(CONFIG.STORAGE.TMP_DIR, basename(previewPath)) + await copyFile(previewPath, tmpPreviewPath) - audioPath: audioInputPath, - resolution, + const transcodeOptions = { + type: 'merge-audio' as 'merge-audio', - job - } + inputPath: tmpPreviewPath, + outputPath: videoTranscodedPath, - try { - await transcodeVOD(transcodeOptions) + inputFileMutexReleaser, - await remove(audioInputPath) - await remove(tmpPreviewPath) - } catch (err) { - await remove(tmpPreviewPath) - throw err - } + availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), + profile: CONFIG.TRANSCODING.PROFILE, - // Important to do this before getVideoFilename() to take in account the new file extension - inputVideoFile.extname = newExtname - inputVideoFile.resolution = resolution - inputVideoFile.filename = generateWebTorrentVideoFilename(inputVideoFile.resolution, newExtname) + audioPath: audioInputPath, + resolution, - const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, inputVideoFile) - // ffmpeg generated a new video file, so update the video duration - // See https://trac.ffmpeg.org/ticket/5456 - video.duration = await getVideoStreamDuration(videoTranscodedPath) - await video.save() + job + } - return onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) - }) + try { + await transcodeVOD(transcodeOptions) + + await remove(audioInputPath) + await remove(tmpPreviewPath) + } catch (err) { + await remove(tmpPreviewPath) + throw err + } + + // Important to do this before getVideoFilename() to take in account the new file extension + inputVideoFile.extname = newExtname + inputVideoFile.resolution = resolution + inputVideoFile.filename = generateWebTorrentVideoFilename(inputVideoFile.resolution, newExtname) + + // ffmpeg generated a new video file, so update the video duration + // See https://trac.ffmpeg.org/ticket/5456 + video.duration = await getVideoStreamDuration(videoTranscodedPath) + await video.save() + + return onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, inputVideoFile) + }) + + return result + } finally { + inputFileMutexReleaser() + } } // Concat TS segments from a live video to a fragmented mp4 HLS playlist @@ -207,13 +250,13 @@ async function generateHlsPlaylistResolutionFromTS (options: { concatenatedTsFilePath: string resolution: VideoResolution isAAC: boolean + inputFileMutexReleaser: MutexInterface.Releaser }) { return generateHlsPlaylistCommon({ - video: options.video, - resolution: options.resolution, - inputPath: options.concatenatedTsFilePath, type: 'hls-from-ts' as 'hls-from-ts', - isAAC: options.isAAC + inputPath: options.concatenatedTsFilePath, + + ...pick(options, [ 'video', 'resolution', 'inputFileMutexReleaser', 'isAAC' ]) }) } @@ -223,15 +266,14 @@ function generateHlsPlaylistResolution (options: { videoInputPath: string resolution: VideoResolution copyCodecs: boolean + inputFileMutexReleaser: MutexInterface.Releaser job?: Job }) { return generateHlsPlaylistCommon({ - video: options.video, - resolution: options.resolution, - copyCodecs: options.copyCodecs, - inputPath: options.videoInputPath, type: 'hls' as 'hls', - job: options.job + inputPath: options.videoInputPath, + + ...pick(options, [ 'video', 'resolution', 'copyCodecs', 'inputFileMutexReleaser', 'job' ]) }) } @@ -251,27 +293,39 @@ async function onWebTorrentVideoFileTranscoding ( video: MVideoFullLight, videoFile: MVideoFile, transcodingPath: string, - outputPath: string + newVideoFile: MVideoFile ) { - const stats = await stat(transcodingPath) - const fps = await getVideoStreamFPS(transcodingPath) - const metadata = await buildFileMetadata(transcodingPath) + const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) + + try { + await video.reload() + + const outputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newVideoFile) - await move(transcodingPath, outputPath, { overwrite: true }) + const stats = await stat(transcodingPath) - videoFile.size = stats.size - videoFile.fps = fps - videoFile.metadata = metadata + const probe = await ffprobePromise(transcodingPath) + const fps = await getVideoStreamFPS(transcodingPath, probe) + const metadata = await buildFileMetadata(transcodingPath, probe) - await createTorrentAndSetInfoHash(video, videoFile) + await move(transcodingPath, outputPath, { overwrite: true }) - const oldFile = await VideoFileModel.loadWebTorrentFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution }) - if (oldFile) await video.removeWebTorrentFile(oldFile) + videoFile.size = stats.size + videoFile.fps = fps + videoFile.metadata = metadata - await VideoFileModel.customUpsert(videoFile, 'video', undefined) - video.VideoFiles = await video.$get('VideoFiles') + await createTorrentAndSetInfoHash(video, videoFile) - return { video, videoFile } + const oldFile = await VideoFileModel.loadWebTorrentFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution }) + if (oldFile) await video.removeWebTorrentFile(oldFile) + + await VideoFileModel.customUpsert(videoFile, 'video', undefined) + video.VideoFiles = await video.$get('VideoFiles') + + return { video, videoFile } + } finally { + mutexReleaser() + } } async function generateHlsPlaylistCommon (options: { @@ -279,12 +333,15 @@ async function generateHlsPlaylistCommon (options: { video: MVideo inputPath: string resolution: VideoResolution + + inputFileMutexReleaser: MutexInterface.Releaser + copyCodecs?: boolean isAAC?: boolean job?: Job }) { - const { type, video, inputPath, resolution, copyCodecs, isAAC, job } = options + const { type, video, inputPath, resolution, copyCodecs, isAAC, job, inputFileMutexReleaser } = options const transcodeDirectory = CONFIG.STORAGE.TMP_DIR const videoTranscodedBasePath = join(transcodeDirectory, type) @@ -308,6 +365,8 @@ async function generateHlsPlaylistCommon (options: { isAAC, + inputFileMutexReleaser, + hlsPlaylist: { videoFilename }, @@ -333,40 +392,54 @@ async function generateHlsPlaylistCommon (options: { videoStreamingPlaylistId: playlist.id }) - const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, newVideoFile) - await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video)) + const mutexReleaser = await VideoPathManager.Instance.lockFiles(video.uuid) - // Move playlist file - const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, resolutionPlaylistFilename) - await move(resolutionPlaylistFileTranscodePath, resolutionPlaylistPath, { overwrite: true }) - // Move video file - await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true }) + try { + // VOD transcoding is a long task, refresh video attributes + await video.reload() - // Update video duration if it was not set (in case of a live for example) - if (!video.duration) { - video.duration = await getVideoStreamDuration(videoFilePath) - await video.save() - } + const videoFilePath = VideoPathManager.Instance.getFSVideoFileOutputPath(playlist, newVideoFile) + await ensureDir(VideoPathManager.Instance.getFSHLSOutputPath(video)) - const stats = await stat(videoFilePath) + // Move playlist file + const resolutionPlaylistPath = VideoPathManager.Instance.getFSHLSOutputPath(video, resolutionPlaylistFilename) + await move(resolutionPlaylistFileTranscodePath, resolutionPlaylistPath, { overwrite: true }) + // Move video file + await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true }) - newVideoFile.size = stats.size - newVideoFile.fps = await getVideoStreamFPS(videoFilePath) - newVideoFile.metadata = await buildFileMetadata(videoFilePath) + // Update video duration if it was not set (in case of a live for example) + if (!video.duration) { + video.duration = await getVideoStreamDuration(videoFilePath) + await video.save() + } - await createTorrentAndSetInfoHash(playlist, newVideoFile) + const stats = await stat(videoFilePath) - const oldFile = await VideoFileModel.loadHLSFile({ playlistId: playlist.id, fps: newVideoFile.fps, resolution: newVideoFile.resolution }) - if (oldFile) { - await video.removeStreamingPlaylistVideoFile(playlist, oldFile) - await oldFile.destroy() - } + newVideoFile.size = stats.size + newVideoFile.fps = await getVideoStreamFPS(videoFilePath) + newVideoFile.metadata = await buildFileMetadata(videoFilePath) + + await createTorrentAndSetInfoHash(playlist, newVideoFile) - const savedVideoFile = await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined) + const oldFile = await VideoFileModel.loadHLSFile({ + playlistId: playlist.id, + fps: newVideoFile.fps, + resolution: newVideoFile.resolution + }) + + if (oldFile) { + await video.removeStreamingPlaylistVideoFile(playlist, oldFile) + await oldFile.destroy() + } - await updatePlaylistAfterFileChange(video, playlist) + const savedVideoFile = await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined) - return { resolutionPlaylistPath, videoFile: savedVideoFile } + await updatePlaylistAfterFileChange(video, playlist) + + return { resolutionPlaylistPath, videoFile: savedVideoFile } + } finally { + mutexReleaser() + } } function buildOriginalFileResolution (inputResolution: number) { diff --git a/server/lib/video-path-manager.ts b/server/lib/video-path-manager.ts index c3f55fd95..9953cae5d 100644 --- a/server/lib/video-path-manager.ts +++ b/server/lib/video-path-manager.ts @@ -1,29 +1,31 @@ +import { Mutex } from 'async-mutex' import { remove } from 'fs-extra' import { extname, join } from 'path' +import { logger, loggerTagsFactory } from '@server/helpers/logger' import { extractVideo } from '@server/helpers/video' import { CONFIG } from '@server/initializers/config' -import { - MStreamingPlaylistVideo, - MVideo, - MVideoFile, - MVideoFileStreamingPlaylistVideo, - MVideoFileVideo, - MVideoUUID -} from '@server/types/models' +import { DIRECTORIES } from '@server/initializers/constants' +import { MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '@server/types/models' import { buildUUID } from '@shared/extra-utils' import { VideoStorage } from '@shared/models' import { makeHLSFileAvailable, makeWebTorrentFileAvailable } from './object-storage' import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from './paths' +import { isVideoInPrivateDirectory } from './video-privacy' type MakeAvailableCB = (path: string) => Promise | T +const lTags = loggerTagsFactory('video-path-manager') + class VideoPathManager { private static instance: VideoPathManager + // Key is a video UUID + private readonly videoFileMutexStore = new Map() + private constructor () {} - getFSHLSOutputPath (video: MVideoUUID, filename?: string) { + getFSHLSOutputPath (video: MVideo, filename?: string) { const base = getHLSDirectory(video) if (!filename) return base @@ -41,13 +43,17 @@ class VideoPathManager { } getFSVideoFileOutputPath (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile) { - if (videoFile.isHLS()) { - const video = extractVideo(videoOrPlaylist) + const video = extractVideo(videoOrPlaylist) + if (videoFile.isHLS()) { return join(getHLSDirectory(video), videoFile.filename) } - return join(CONFIG.STORAGE.VIDEOS_DIR, videoFile.filename) + if (isVideoInPrivateDirectory(video.privacy)) { + return join(DIRECTORIES.VIDEOS.PRIVATE, videoFile.filename) + } + + return join(DIRECTORIES.VIDEOS.PUBLIC, videoFile.filename) } async makeAvailableVideoFile (videoFile: MVideoFileVideo | MVideoFileStreamingPlaylistVideo, cb: MakeAvailableCB) { @@ -113,6 +119,27 @@ class VideoPathManager { ) } + async lockFiles (videoUUID: string) { + if (!this.videoFileMutexStore.has(videoUUID)) { + this.videoFileMutexStore.set(videoUUID, new Mutex()) + } + + const mutex = this.videoFileMutexStore.get(videoUUID) + const releaser = await mutex.acquire() + + logger.debug('Locked files of %s.', videoUUID, lTags(videoUUID)) + + return releaser + } + + unlockFiles (videoUUID: string) { + const mutex = this.videoFileMutexStore.get(videoUUID) + + mutex.release() + + logger.debug('Released lockfiles of %s.', videoUUID, lTags(videoUUID)) + } + private async makeAvailableFactory (method: () => Promise | string, clean: boolean, cb: MakeAvailableCB) { let result: T diff --git a/server/lib/video-privacy.ts b/server/lib/video-privacy.ts new file mode 100644 index 000000000..1a4a5a22d --- /dev/null +++ b/server/lib/video-privacy.ts @@ -0,0 +1,96 @@ +import { move } from 'fs-extra' +import { join } from 'path' +import { logger } from '@server/helpers/logger' +import { DIRECTORIES } from '@server/initializers/constants' +import { MVideo, MVideoFullLight } from '@server/types/models' +import { VideoPrivacy } from '@shared/models' + +function setVideoPrivacy (video: MVideo, newPrivacy: VideoPrivacy) { + if (video.privacy === VideoPrivacy.PRIVATE && newPrivacy !== VideoPrivacy.PRIVATE) { + video.publishedAt = new Date() + } + + video.privacy = newPrivacy +} + +function isVideoInPrivateDirectory (privacy: VideoPrivacy) { + return privacy === VideoPrivacy.PRIVATE || privacy === VideoPrivacy.INTERNAL +} + +function isVideoInPublicDirectory (privacy: VideoPrivacy) { + return !isVideoInPrivateDirectory(privacy) +} + +async function moveFilesIfPrivacyChanged (video: MVideoFullLight, oldPrivacy: VideoPrivacy) { + // Now public, previously private + if (isVideoInPublicDirectory(video.privacy) && isVideoInPrivateDirectory(oldPrivacy)) { + await moveFiles({ type: 'private-to-public', video }) + + return true + } + + // Now private, previously public + if (isVideoInPrivateDirectory(video.privacy) && isVideoInPublicDirectory(oldPrivacy)) { + await moveFiles({ type: 'public-to-private', video }) + + return true + } + + return false +} + +export { + setVideoPrivacy, + + isVideoInPrivateDirectory, + isVideoInPublicDirectory, + + moveFilesIfPrivacyChanged +} + +// --------------------------------------------------------------------------- + +async function moveFiles (options: { + type: 'private-to-public' | 'public-to-private' + video: MVideoFullLight +}) { + const { type, video } = options + + const directories = type === 'private-to-public' + ? { + webtorrent: { old: DIRECTORIES.VIDEOS.PRIVATE, new: DIRECTORIES.VIDEOS.PUBLIC }, + hls: { old: DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE, new: DIRECTORIES.HLS_STREAMING_PLAYLIST.PUBLIC } + } + : { + webtorrent: { old: DIRECTORIES.VIDEOS.PUBLIC, new: DIRECTORIES.VIDEOS.PRIVATE }, + hls: { old: DIRECTORIES.HLS_STREAMING_PLAYLIST.PUBLIC, new: DIRECTORIES.HLS_STREAMING_PLAYLIST.PRIVATE } + } + + for (const file of video.VideoFiles) { + const source = join(directories.webtorrent.old, file.filename) + const destination = join(directories.webtorrent.new, file.filename) + + try { + logger.info('Moving WebTorrent files of %s after privacy change (%s -> %s).', video.uuid, source, destination) + + await move(source, destination) + } catch (err) { + logger.error('Cannot move webtorrent file %s to %s after privacy change', source, destination, { err }) + } + } + + const hls = video.getHLSPlaylist() + + if (hls) { + const source = join(directories.hls.old, video.uuid) + const destination = join(directories.hls.new, video.uuid) + + try { + logger.info('Moving HLS files of %s after privacy change (%s -> %s).', video.uuid, source, destination) + + await move(source, destination) + } catch (err) { + logger.error('Cannot move HLS file %s to %s after privacy change', source, destination, { err }) + } + } +} diff --git a/server/lib/video-tokens-manager.ts b/server/lib/video-tokens-manager.ts new file mode 100644 index 000000000..c43085d16 --- /dev/null +++ b/server/lib/video-tokens-manager.ts @@ -0,0 +1,49 @@ +import LRUCache from 'lru-cache' +import { LRU_CACHE } from '@server/initializers/constants' +import { buildUUID } from '@shared/extra-utils' + +// --------------------------------------------------------------------------- +// Create temporary tokens that can be used as URL query parameters to access video static files +// --------------------------------------------------------------------------- + +class VideoTokensManager { + + private static instance: VideoTokensManager + + private readonly lruCache = new LRUCache({ + max: LRU_CACHE.VIDEO_TOKENS.MAX_SIZE, + ttl: LRU_CACHE.VIDEO_TOKENS.TTL + }) + + private constructor () {} + + create (videoUUID: string) { + const token = buildUUID() + + const expires = new Date(new Date().getTime() + LRU_CACHE.VIDEO_TOKENS.TTL) + + this.lruCache.set(token, videoUUID) + + return { token, expires } + } + + hasToken (options: { + token: string + videoUUID: string + }) { + const value = this.lruCache.get(options.token) + if (!value) return false + + return value === options.videoUUID + } + + static get Instance () { + return this.instance || (this.instance = new this()) + } +} + +// --------------------------------------------------------------------------- + +export { + VideoTokensManager +} diff --git a/server/lib/video.ts b/server/lib/video.ts index 6c4f3ce7b..aacc41a7a 100644 --- a/server/lib/video.ts +++ b/server/lib/video.ts @@ -7,10 +7,11 @@ import { TagModel } from '@server/models/video/tag' import { VideoModel } from '@server/models/video/video' import { VideoJobInfoModel } from '@server/models/video/video-job-info' import { FilteredModelAttributes } from '@server/types' -import { MThumbnail, MUserId, MVideoFile, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models' -import { ThumbnailType, VideoCreate, VideoPrivacy, VideoState, VideoTranscodingPayload } from '@shared/models' -import { CreateJobOptions } from './job-queue/job-queue' +import { MThumbnail, MUserId, MVideoFile, MVideoFullLight, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models' +import { ManageVideoTorrentPayload, ThumbnailType, VideoCreate, VideoPrivacy, VideoState, VideoTranscodingPayload } from '@shared/models' +import { CreateJobArgument, CreateJobOptions, JobQueue } from './job-queue/job-queue' import { updateVideoMiniatureFromExisting } from './thumbnail' +import { moveFilesIfPrivacyChanged } from './video-privacy' function buildLocalVideoFromReq (videoInfo: VideoCreate, channelId: number): FilteredModelAttributes { return { @@ -177,6 +178,59 @@ const getCachedVideoDuration = memoizee(getVideoDuration, { // --------------------------------------------------------------------------- +async function addVideoJobsAfterUpdate (options: { + video: MVideoFullLight + isNewVideo: boolean + + nameChanged: boolean + oldPrivacy: VideoPrivacy +}) { + const { video, nameChanged, oldPrivacy, isNewVideo } = options + const jobs: CreateJobArgument[] = [] + + const filePathChanged = await moveFilesIfPrivacyChanged(video, oldPrivacy) + + if (!video.isLive && (nameChanged || filePathChanged)) { + for (const file of (video.VideoFiles || [])) { + const payload: ManageVideoTorrentPayload = { action: 'update-metadata', videoId: video.id, videoFileId: file.id } + + jobs.push({ type: 'manage-video-torrent', payload }) + } + + const hls = video.getHLSPlaylist() + + for (const file of (hls?.VideoFiles || [])) { + const payload: ManageVideoTorrentPayload = { action: 'update-metadata', streamingPlaylistId: hls.id, videoFileId: file.id } + + jobs.push({ type: 'manage-video-torrent', payload }) + } + } + + jobs.push({ + type: 'federate-video', + payload: { + videoUUID: video.uuid, + isNewVideo + } + }) + + const wasConfidentialVideo = new Set([ VideoPrivacy.PRIVATE, VideoPrivacy.UNLISTED, VideoPrivacy.INTERNAL ]).has(oldPrivacy) + + if (wasConfidentialVideo) { + jobs.push({ + type: 'notify', + payload: { + action: 'new-video', + videoUUID: video.uuid + } + }) + } + + return JobQueue.Instance.createSequentialJobFlow(...jobs) +} + +// --------------------------------------------------------------------------- + export { buildLocalVideoFromReq, buildVideoThumbnailsFromReq, @@ -185,5 +239,6 @@ export { buildTranscodingJob, buildMoveToObjectStorageJob, getTranscodingJobPriority, + addVideoJobsAfterUpdate, getCachedVideoDuration } -- cgit v1.2.3