|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301 |
- import { buildAspectRatio } from '@peertube/peertube-core-utils'
- import {
- LiveVideoCreate,
- LiveVideoLatencyMode,
- ThumbnailType,
- ThumbnailType_Type,
- VideoCreate,
- VideoPrivacy,
- VideoStateType
- } from '@peertube/peertube-models'
- import { buildUUID } from '@peertube/peertube-node-utils'
- import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
- import { LoggerTagsFn, logger } from '@server/helpers/logger.js'
- import { CONFIG } from '@server/initializers/config.js'
- import { sequelizeTypescript } from '@server/initializers/database.js'
- import { ScheduleVideoUpdateModel } from '@server/models/video/schedule-video-update.js'
- import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting.js'
- import { VideoLiveModel } from '@server/models/video/video-live.js'
- import { VideoPasswordModel } from '@server/models/video/video-password.js'
- import { VideoModel } from '@server/models/video/video.js'
- import { MChannel, MChannelAccountLight, MThumbnail, MUser, MVideoFile, MVideoFullLight } from '@server/types/models/index.js'
- import { FilteredModelAttributes } from '@server/types/sequelize.js'
- import { FfprobeData } from 'fluent-ffmpeg'
- import { move } from 'fs-extra/esm'
- import { getLocalVideoActivityPubUrl } from './activitypub/url.js'
- import { federateVideoIfNeeded } from './activitypub/videos/federate.js'
- import { AutomaticTagger } from './automatic-tags/automatic-tagger.js'
- import { setAndSaveVideoAutomaticTags } from './automatic-tags/automatic-tags.js'
- import { Hooks } from './plugins/hooks.js'
- import { generateLocalVideoMiniature, updateLocalVideoMiniatureFromExisting } from './thumbnail.js'
- import { autoBlacklistVideoIfNeeded } from './video-blacklist.js'
- import { replaceChapters, replaceChaptersFromDescriptionIfNeeded } from './video-chapters.js'
- import { buildNewFile, createVideoSource } from './video-file.js'
- import { addVideoJobsAfterCreation } from './video-jobs.js'
- import { VideoPathManager } from './video-path-manager.js'
- import { buildCommentsPolicy, setVideoTags } from './video.js'
-
- type VideoAttributes = Omit<VideoCreate, 'channelId'> & {
- duration: number
- isLive: boolean
- state: VideoStateType
- inputFilename: string
- }
-
- type LiveAttributes = Pick<LiveVideoCreate, 'permanentLive' | 'latencyMode' | 'saveReplay' | 'replaySettings'> & {
- streamKey?: string
- }
-
- export type ThumbnailOptions = {
- path: string
- type: ThumbnailType_Type
- automaticallyGenerated: boolean
- keepOriginal: boolean
- }[]
-
- type ChaptersOption = { timecode: number, title: string }[]
-
- type VideoAttributeHookFilter =
- 'filter:api.video.user-import.video-attribute.result' |
- 'filter:api.video.upload.video-attribute.result' |
- 'filter:api.video.live.video-attribute.result'
-
- export class LocalVideoCreator {
- private readonly lTags: LoggerTagsFn
-
- private readonly videoFilePath: string | undefined
- private readonly videoFileProbe: FfprobeData
-
- private readonly videoAttributes: VideoAttributes
- private readonly liveAttributes: LiveAttributes | undefined
-
- private readonly channel: MChannelAccountLight
- private readonly videoAttributeResultHook: VideoAttributeHookFilter
-
- private video: MVideoFullLight
- private videoFile: MVideoFile
- private videoPath: string
-
- constructor (private readonly options: {
- lTags: LoggerTagsFn
-
- videoFile: {
- path: string
- probe: FfprobeData
- }
-
- videoAttributes: VideoAttributes
- liveAttributes: LiveAttributes
-
- channel: MChannelAccountLight
- user: MUser
- videoAttributeResultHook: VideoAttributeHookFilter
- thumbnails: ThumbnailOptions
-
- chapters: ChaptersOption | undefined
- fallbackChapters: {
- fromDescription: boolean
- finalFallback: ChaptersOption | undefined
- }
- }) {
- this.videoFilePath = options.videoFile?.path
- this.videoFileProbe = options.videoFile?.probe
-
- this.videoAttributes = options.videoAttributes
- this.liveAttributes = options.liveAttributes
-
- this.channel = options.channel
-
- this.videoAttributeResultHook = options.videoAttributeResultHook
-
- this.lTags = options.lTags
- }
-
- async create () {
- this.video = new VideoModel(
- await Hooks.wrapObject(this.buildVideo(this.videoAttributes, this.channel), this.videoAttributeResultHook)
- ) as MVideoFullLight
-
- this.video.VideoChannel = this.channel
- this.video.url = getLocalVideoActivityPubUrl(this.video)
-
- if (this.videoFilePath) {
- this.videoFile = await buildNewFile({ path: this.videoFilePath, mode: 'web-video', ffprobe: this.videoFileProbe })
-
- this.videoPath = VideoPathManager.Instance.getFSVideoFileOutputPath(this.video, this.videoFile)
- await move(this.videoFilePath, this.videoPath)
-
- this.video.aspectRatio = buildAspectRatio({ width: this.videoFile.width, height: this.videoFile.height })
- }
-
- const thumbnails = await this.createThumbnails()
-
- await retryTransactionWrapper(() => {
- return sequelizeTypescript.transaction(async transaction => {
- await this.video.save({ transaction })
-
- for (const thumbnail of thumbnails) {
- await this.video.addAndSaveThumbnail(thumbnail, transaction)
- }
-
- if (this.videoFile) {
- this.videoFile.videoId = this.video.id
- await this.videoFile.save({ transaction })
-
- this.video.VideoFiles = [ this.videoFile ]
- }
-
- await setVideoTags({ video: this.video, tags: this.videoAttributes.tags, transaction })
-
- const automaticTags = await new AutomaticTagger().buildVideoAutomaticTags({ video: this.video, transaction })
- await setAndSaveVideoAutomaticTags({ video: this.video, automaticTags, transaction })
-
- // Schedule an update in the future?
- if (this.videoAttributes.scheduleUpdate) {
- await ScheduleVideoUpdateModel.create({
- videoId: this.video.id,
- updateAt: new Date(this.videoAttributes.scheduleUpdate.updateAt),
- privacy: this.videoAttributes.scheduleUpdate.privacy || null
- }, { transaction })
- }
-
- if (this.options.chapters) {
- await replaceChapters({ video: this.video, chapters: this.options.chapters, transaction })
- } else if (this.options.fallbackChapters.fromDescription) {
- if (!await replaceChaptersFromDescriptionIfNeeded({ newDescription: this.video.description, video: this.video, transaction })) {
- await replaceChapters({ video: this.video, chapters: this.options.fallbackChapters.finalFallback, transaction })
- }
- }
-
- await autoBlacklistVideoIfNeeded({
- video: this.video,
- user: this.options.user,
- isRemote: false,
- isNew: true,
- isNewFile: true,
- transaction
- })
-
- if (this.videoAttributes.privacy === VideoPrivacy.PASSWORD_PROTECTED) {
- await VideoPasswordModel.addPasswords(this.videoAttributes.videoPasswords, this.video.id, transaction)
- }
-
- if (this.videoAttributes.isLive) {
- const videoLive = new VideoLiveModel({
- saveReplay: this.liveAttributes.saveReplay || false,
- permanentLive: this.liveAttributes.permanentLive || false,
- latencyMode: this.liveAttributes.latencyMode || LiveVideoLatencyMode.DEFAULT,
- streamKey: this.liveAttributes.streamKey || buildUUID()
- })
-
- if (videoLive.saveReplay) {
- const replaySettings = new VideoLiveReplaySettingModel({
- privacy: this.liveAttributes.replaySettings?.privacy ?? this.video.privacy
- })
- await replaySettings.save({ transaction })
-
- videoLive.replaySettingId = replaySettings.id
- }
-
- videoLive.videoId = this.video.id
- this.video.VideoLive = await videoLive.save({ transaction })
- }
-
- if (this.videoFile) {
- transaction.afterCommit(() => {
- addVideoJobsAfterCreation({
- video: this.video,
- videoFile: this.videoFile,
- generateTranscription: this.videoAttributes.generateTranscription ?? true
- }).catch(err => logger.error('Cannot build new video jobs of %s.', this.video.uuid, { err, ...this.lTags(this.video.uuid) }))
- })
- } else {
- await federateVideoIfNeeded(this.video, true, transaction)
- }
- }).catch(err => {
- // Reset elements to reinsert them in the database
- this.video.isNewRecord = true
- if (this.videoFile) this.videoFile.isNewRecord = true
-
- for (const t of thumbnails) {
- t.isNewRecord = true
- }
-
- throw err
- })
- })
-
- if (this.videoAttributes.inputFilename) {
- await createVideoSource({
- inputFilename: this.videoAttributes.inputFilename,
- inputPath: this.videoPath,
- inputProbe: this.videoFileProbe,
- video: this.video
- })
- }
-
- // Channel has a new content, set as updated
- await this.channel.setAsUpdated()
-
- return { video: this.video, videoFile: this.videoFile }
- }
-
- private async createThumbnails () {
- const promises: Promise<MThumbnail>[] = []
- let toGenerate = [ ThumbnailType.MINIATURE, ThumbnailType.PREVIEW ]
-
- for (const type of [ ThumbnailType.MINIATURE, ThumbnailType.PREVIEW ]) {
- const thumbnail = this.options.thumbnails.find(t => t.type === type)
- if (!thumbnail) continue
-
- promises.push(
- updateLocalVideoMiniatureFromExisting({
- inputPath: thumbnail.path,
- video: this.video,
- type,
- automaticallyGenerated: thumbnail.automaticallyGenerated || false,
- keepOriginal: thumbnail.keepOriginal
- })
- )
-
- toGenerate = toGenerate.filter(t => t !== thumbnail.type)
- }
-
- return [
- ...await Promise.all(promises),
-
- ...await generateLocalVideoMiniature({
- video: this.video,
- videoFile: this.videoFile,
- types: toGenerate,
- ffprobe: this.videoFileProbe
- })
- ]
- }
-
- private buildVideo (videoInfo: VideoAttributes, channel: MChannel): FilteredModelAttributes<VideoModel> {
- return {
- name: videoInfo.name,
- state: videoInfo.state,
- remote: false,
- category: videoInfo.category,
- licence: videoInfo.licence ?? CONFIG.DEFAULTS.PUBLISH.LICENCE,
- language: videoInfo.language,
- commentsPolicy: buildCommentsPolicy(videoInfo),
- downloadEnabled: videoInfo.downloadEnabled ?? CONFIG.DEFAULTS.PUBLISH.DOWNLOAD_ENABLED,
- waitTranscoding: videoInfo.waitTranscoding || false,
- nsfw: videoInfo.nsfw || false,
- description: videoInfo.description,
- support: videoInfo.support,
- privacy: videoInfo.privacy || VideoPrivacy.PRIVATE,
- isLive: videoInfo.isLive,
- channelId: channel.id,
- originallyPublishedAt: videoInfo.originallyPublishedAt
- ? new Date(videoInfo.originallyPublishedAt)
- : null,
-
- uuid: buildUUID(),
- duration: videoInfo.duration
- }
- }
- }
|