1
0
Fork 0
mirror of https://github.com/Chocobozzz/PeerTube.git synced 2025-10-03 01:39:37 +02:00
Peertube/client/src/app/+videos-publish-manage/shared-manage/common/video-edit.model.ts
2025-09-12 08:54:50 +02:00

1084 lines
31 KiB
TypeScript

import { getOriginUrl } from '@app/helpers'
import { exists, omit, pick, secondsToTime } from '@peertube/peertube-core-utils'
import {
HTMLServerConfig,
LiveVideo,
LiveVideoCreate,
LiveVideoUpdate,
NSFWFlag,
PlayerVideoSettings,
PlayerVideoSettingsUpdate,
VideoCaption,
VideoChapter,
VideoCreate,
VideoDetails,
VideoImportCreate,
VideoPrivacy,
VideoPrivacyType,
VideoScheduleUpdate,
VideoSource,
VideoState,
VideoStateType,
VideoStudioTask,
VideoStudioTaskCut,
VideoUpdate
} from '@peertube/peertube-models'
import { logger } from '@root-helpers/logger'
import debug from 'debug'
import { Jsonify, SharedUnionFieldsDeep } from 'type-fest'
import { VideoCaptionWithPathEdit } from './video-caption-edit.model'
import { VideoChaptersEdit } from './video-chapters-edit.model'
const debugLogger = debug('peertube:video-manage:video-edit')
export type VideoEditPrivacyType = VideoPrivacyType | typeof VideoEdit.SPECIAL_SCHEDULED_PRIVACY
type CommonUpdateForm =
& Omit<
VideoUpdate,
'privacy' | 'videoPasswords' | 'thumbnailfile' | 'scheduleUpdate' | 'commentsEnabled' | 'originallyPublishedAt' | 'nsfwFlags'
>
& {
schedulePublicationAt?: Date
originallyPublishedAt?: Date
privacy?: VideoEditPrivacyType
videoPassword?: string
nsfwFlagViolent?: boolean
nsfwFlagSex?: boolean
}
type LiveUpdateForm = Omit<LiveVideoUpdate, 'replaySettings' | 'schedules'> & {
replayPrivacy?: VideoPrivacyType
liveStreamKey?: string
schedules?: {
startAt?: Date
}[]
}
type ReplaceFileForm = {
replaceFile?: File
}
type StudioForm = {
'cut'?: { start?: number, end?: number }
'add-intro'?: { file?: File }
'add-outro'?: { file?: File }
'add-watermark'?: { file?: File }
}
type PlayerSettingsForm = PlayerVideoSettingsUpdate
// ---------------------------------------------------------------------------
type LoadFromPublishOptions = Required<Pick<VideoCreate, 'channelId' | 'support'>> & Partial<Pick<VideoCreate, 'name'>>
type CreateFromUploadOptions = LoadFromPublishOptions & Required<Pick<VideoCreate, 'name'>>
type CreateFromImportOptions = LoadFromPublishOptions & Pick<VideoImportCreate, 'magnetUri' | 'torrentfile' | 'targetUrl'>
type CreateFromLiveOptions =
& CreateFromUploadOptions
& Required<Pick<LiveVideoCreate, 'permanentLive' | 'latencyMode' | 'saveReplay' | 'replaySettings' | 'schedules'>>
type UpdateFromAPIOptions = {
video?: Pick<
VideoDetails,
| 'id'
| 'uuid'
| 'shortUUID'
| 'name'
| 'channel'
| 'privacy'
| 'category'
| 'licence'
| 'language'
| 'description'
| 'tags'
| 'nsfw'
| 'nsfwFlags'
| 'nsfwSummary'
| 'waitTranscoding'
| 'support'
| 'commentsPolicy'
| 'downloadEnabled'
| 'pluginData'
| 'scheduledUpdate'
| 'originallyPublishedAt'
| 'duration'
| 'likes'
| 'aspectRatio'
| 'views'
| 'blacklisted'
| 'previewPath'
| 'state'
| 'isLive'
>
live?: LiveVideo
chapters?: VideoChapter[]
captions?: VideoCaption[]
videoPasswords?: string[]
videoSource?: VideoSource
playerSettings?: PlayerVideoSettings
}
// ---------------------------------------------------------------------------
type CommonUpdate = Omit<VideoUpdate, 'thumbnailfile' | 'originallyPublishedAt' | 'scheduleUpdate'> & {
originallyPublishedAt?: string
scheduleUpdate?: {
updateAt: string
privacy?: VideoScheduleUpdate['privacy']
}
}
type LiveUpdate = Omit<LiveVideoUpdate, 'schedules'> & {
schedules?: {
startAt: string
}[]
}
export class VideoEdit {
static readonly SPECIAL_SCHEDULED_PRIVACY = -1
private isNewVideo = false
private common: CommonUpdate = {}
private captions: VideoCaptionWithPathEdit[] = []
private chapters: VideoChaptersEdit = new VideoChaptersEdit()
private live: LiveUpdate
private replaceFile: File
private studioTasks: VideoStudioTask[] = []
private playerSettings: PlayerVideoSettings
private videoImport: Pick<VideoImportCreate, 'magnetUri' | 'torrentfile' | 'targetUrl'>
private metadata: Partial<{
id: number
uuid: string
shortUUID: string
state: VideoStateType
isLive: boolean
views: number
aspectRatio: number
duration: number
likes: number
blacklisted: boolean
live: Pick<LiveVideo, 'rtmpUrl' | 'rtmpsUrl' | 'streamKey'>
videoSource: VideoSource
}> = {}
private videoAttributes: {
id: number
shortUUID: string
uuid: string
name: string
state: VideoStateType
privacy: VideoEditPrivacyType
isLive: boolean
aspectRatio: number
duration: number
views: number
likes: number
blacklisted: boolean
live?: Pick<LiveVideo, 'rtmpUrl' | 'rtmpsUrl' | 'streamKey'>
}
private saveStore: {
common?: Omit<CommonUpdate, 'pluginData' | 'previewfile'>
previewfile?: { size: number }
live?: LiveUpdate
playerSettings?: PlayerVideoSettings
pluginData?: any
pluginDefaults?: Record<string, string | boolean>
} = {}
private checkPluginChanges = false
private serverConfig: HTMLServerConfig
private constructor (serverConfig: HTMLServerConfig, isNewVideo = false) {
this.serverConfig = serverConfig
this.isNewVideo = isNewVideo
}
// ---------------------------------------------------------------------------
static createFromUpload (serverConfig: HTMLServerConfig, options: CreateFromUploadOptions) {
const videoEdit = new VideoEdit(serverConfig, true)
videoEdit.loadFromPublish(options, false)
return videoEdit
}
// ---------------------------------------------------------------------------
static createFromImport (serverConfig: HTMLServerConfig, options: CreateFromImportOptions) {
const videoEdit = new VideoEdit(serverConfig, true)
videoEdit.loadFromImport(options)
return videoEdit
}
private loadFromImport (options: CreateFromImportOptions) {
this.loadFromPublish(options, false)
this.videoImport = {
targetUrl: options.targetUrl,
magnetUri: options.magnetUri,
torrentfile: options.torrentfile
}
this.updateAfterChange()
}
// ---------------------------------------------------------------------------
static createFromLive (serverConfig: HTMLServerConfig, options: CreateFromLiveOptions) {
const videoEdit = new VideoEdit(serverConfig, true)
videoEdit.loadFromLive(options)
return videoEdit
}
private loadFromLive (options: CreateFromLiveOptions) {
this.loadFromPublish(options, true)
this.live = {
latencyMode: options.latencyMode,
permanentLive: options.permanentLive,
saveReplay: options.saveReplay,
replaySettings: options.replaySettings
? { privacy: options.replaySettings.privacy }
: undefined,
schedules: options.schedules
? options.schedules.map(s => ({ startAt: new Date(s.startAt).toISOString() }))
: undefined
}
this.updateAfterChange()
}
// ---------------------------------------------------------------------------
private loadFromPublish (options: LoadFromPublishOptions, isLive: boolean) {
const serverDefaults = this.serverConfig.defaults
this.common.name = options.name
this.common.channelId = options.channelId
this.common.support = options.support
this.metadata.isLive = isLive
this.common.privacy = serverDefaults.publish.privacy
this.common.downloadEnabled = serverDefaults.publish.downloadEnabled
this.common.licence = serverDefaults.publish.licence
this.common.commentsPolicy = serverDefaults.publish.commentsPolicy
this.common.nsfw = this.serverConfig.instance.isNSFW
this.common.waitTranscoding = true
this.common.tags = []
this.common.pluginData = {}
this.metadata.views = 0
this.metadata.likes = 0
this.updateAfterChange()
}
// ---------------------------------------------------------------------------
static async createFromAPI (serverConfig: HTMLServerConfig, options: UpdateFromAPIOptions) {
const videoEdit = new VideoEdit(serverConfig)
await videoEdit.loadFromAPI(options)
return videoEdit
}
async loadFromAPI (options: UpdateFromAPIOptions & { loadPrivacy?: boolean }) {
const { video, videoPasswords, live, chapters, captions, videoSource, playerSettings, loadPrivacy = true } = options
debugLogger('Load from API', options)
this.loadVideo({ video, videoPasswords, saveInStore: true, loadPrivacy })
this.loadLive(live)
this.loadPlayerSettings(playerSettings)
if (captions !== undefined) {
this.captions = captions
}
if (chapters !== undefined) {
this.chapters = new VideoChaptersEdit()
if (chapters) this.chapters.loadFromAPI(chapters)
}
if (videoSource !== undefined) {
this.metadata.videoSource = videoSource
}
await this.loadPreview(video)
this.updateAfterChange()
}
private loadVideo (options: {
video: UpdateFromAPIOptions['video']
videoPasswords?: string[]
loadPrivacy?: boolean // default true
saveInStore: boolean
}) {
const { video, saveInStore, loadPrivacy = true, videoPasswords = [] } = options
if (video === undefined) return
const buildObj: (options: { loadPrivacy: boolean }) => CommonUpdate = () => {
const { loadPrivacy } = options
const base = {
...this.common,
name: video.name || '',
channelId: video.channel?.id ?? null,
category: video.category?.id ?? null,
licence: video.licence?.id ?? null,
language: video.language?.id ?? null,
description: video.description ?? '',
tags: video.tags ?? [],
nsfw: video.nsfw ?? null,
nsfwSummary: video.nsfwSummary ?? null,
nsfwFlags: video.nsfwFlags ?? NSFWFlag.NONE,
waitTranscoding: video.waitTranscoding ?? null,
support: video.support ?? '',
commentsPolicy: video.commentsPolicy?.id ?? null,
downloadEnabled: video.downloadEnabled ?? null,
pluginData: video.pluginData ?? {},
scheduleUpdate: video.scheduledUpdate
? { updateAt: new Date(video.scheduledUpdate.updateAt).toISOString(), privacy: video.scheduledUpdate.privacy }
: null,
originallyPublishedAt: video.originallyPublishedAt
? new Date(video.originallyPublishedAt).toISOString()
: null,
videoPasswords: videoPasswords ?? []
}
if (loadPrivacy) {
return { ...base, privacy: video.privacy?.id ?? null }
}
return base
}
this.common = buildObj({ loadPrivacy })
if (saveInStore) {
const obj = buildObj({ loadPrivacy: true })
this.saveStore.common = omit(obj, [ 'pluginData', 'previewfile' ])
// Apply plugin defaults so we correctly detect changes
const pluginDefaults = this.saveStore.pluginDefaults || {}
this.saveStore.pluginData = { ...pluginDefaults, ...obj.pluginData }
}
// ---------------------------------------------------------------------------
this.metadata.id = video.id
this.metadata.uuid = video.uuid
this.metadata.shortUUID = video.shortUUID
this.metadata.state = video.state.id
this.metadata.duration = video.duration
this.metadata.views = video.views
this.metadata.likes = video.likes
this.metadata.aspectRatio = video.aspectRatio
this.metadata.blacklisted = video.blacklisted
this.metadata.isLive = video.isLive
}
loadPluginDataDefaults (pluginDefaults: Record<string, string | boolean>) {
this.saveStore.pluginDefaults = pluginDefaults
if (this.saveStore?.pluginData) {
this.saveStore.pluginData = { ...this.saveStore.pluginDefaults, ...this.saveStore.pluginData }
}
}
private async loadPreview (video: UpdateFromAPIOptions['video']) {
if (!video?.previewPath) return
try {
const response = await fetch(getOriginUrl() + video.previewPath)
this.common.previewfile = await response.blob()
this.saveStore.previewfile = { size: this.common.previewfile.size }
} catch (err) {
logger.error('Failed to fetch video preview', err)
}
}
private loadLive (live: UpdateFromAPIOptions['live']) {
if (live === undefined) {
this.metadata.isLive = false
return
}
const buildObj = () => {
return {
permanentLive: live.permanentLive,
latencyMode: live.latencyMode,
saveReplay: live.saveReplay,
replaySettings: live.replaySettings
? { privacy: live.replaySettings.privacy }
: undefined,
schedules: live.schedules
? live.schedules.map(s => ({ startAt: new Date(s.startAt).toISOString() }))
: undefined
}
}
this.metadata.isLive = true
this.live = buildObj()
this.saveStore.live = buildObj()
this.metadata.live = pick(live, [ 'rtmpUrl', 'rtmpsUrl', 'streamKey' ])
}
private loadPlayerSettings (playerSettings: UpdateFromAPIOptions['playerSettings']) {
const buildObj = () => {
return {
theme: playerSettings.theme
}
}
this.playerSettings = buildObj()
this.saveStore.playerSettings = buildObj()
}
loadAfterPublish (options: {
video: Pick<VideoDetails, 'id' | 'uuid' | 'shortUUID'>
}) {
this.metadata.id = options.video.id
this.metadata.uuid = options.video.uuid
this.metadata.shortUUID = options.video.shortUUID
this.updateAfterChange()
}
// ---------------------------------------------------------------------------
loadFromCommonForm (values: CommonUpdateForm) {
if (values.name !== undefined) this.common.name = values.name
if (values.channelId !== undefined) this.common.channelId = values.channelId
if (values.category !== undefined) this.common.category = values.category
if (values.licence !== undefined) this.common.licence = values.licence
if (values.language !== undefined) this.common.language = values.language
if (values.description !== undefined) this.common.description = values.description
if (values.tags !== undefined) this.common.tags = values.tags
if (values.waitTranscoding !== undefined) this.common.waitTranscoding = values.waitTranscoding
if (values.support !== undefined) this.common.support = values.support
if (values.commentsPolicy !== undefined) this.common.commentsPolicy = values.commentsPolicy
if (values.downloadEnabled !== undefined) this.common.downloadEnabled = values.downloadEnabled
if (values.previewfile !== undefined) this.common.previewfile = values.previewfile
if (values.pluginData !== undefined) this.common.pluginData = values.pluginData
// ---------------------------------------------------------------------------
// NSFW
// ---------------------------------------------------------------------------
if (values.nsfw !== undefined) this.common.nsfw = values.nsfw
if (this.common.nsfw) {
if (values.nsfwFlagSex !== undefined) {
this.common.nsfwFlags = values.nsfwFlagSex
? this.common.nsfwFlags | NSFWFlag.EXPLICIT_SEX
: this.common.nsfwFlags & ~NSFWFlag.EXPLICIT_SEX
}
if (values.nsfwFlagViolent !== undefined) {
this.common.nsfwFlags = values.nsfwFlagViolent
? this.common.nsfwFlags | NSFWFlag.VIOLENT
: this.common.nsfwFlags & ~NSFWFlag.VIOLENT
}
if (values.nsfwSummary !== undefined) {
this.common.nsfwSummary = values.nsfwSummary
}
} else {
this.common.nsfwSummary = null
this.common.nsfwFlags = NSFWFlag.NONE
}
// ---------------------------------------------------------------------------
if (values.videoPassword !== undefined) {
this.common.videoPasswords = values.privacy === VideoPrivacy.PASSWORD_PROTECTED && values.videoPassword
? [ values.videoPassword ]
: []
}
if (values.privacy !== undefined) {
// If schedule publication, the video is private and will be changed to public privacy
if (values.privacy === VideoEdit.SPECIAL_SCHEDULED_PRIVACY) {
const updateAt = new Date(values.schedulePublicationAt)
updateAt.setSeconds(0)
this.common.privacy = VideoPrivacy.PRIVATE
this.common.scheduleUpdate = {
updateAt: values.schedulePublicationAt
? updateAt.toISOString()
: undefined,
privacy: VideoPrivacy.PUBLIC
}
} else {
this.common.privacy = values.privacy
this.common.scheduleUpdate = null
}
}
// Convert originallyPublishedAt to string so that function objectToFormData() works correctly
if (values.originallyPublishedAt !== undefined) {
this.common.originallyPublishedAt = values.originallyPublishedAt
? new Date(values.originallyPublishedAt).toISOString()
: null
}
this.updateAfterChange()
}
toCommonFormPatch () {
const json: Required<CommonUpdateForm> = {
category: this.common.category,
licence: this.common.licence,
language: this.common.language,
description: this.common.description,
support: this.common.support,
name: this.common.name,
tags: this.common.tags,
nsfw: this.common.nsfw,
nsfwFlagSex: (this.common.nsfwFlags & NSFWFlag.EXPLICIT_SEX) === NSFWFlag.EXPLICIT_SEX,
nsfwFlagViolent: (this.common.nsfwFlags & NSFWFlag.VIOLENT) === NSFWFlag.VIOLENT,
nsfwSummary: this.common.nsfwSummary,
commentsPolicy: this.common.commentsPolicy,
waitTranscoding: this.common.waitTranscoding,
channelId: this.common.channelId,
privacy: this.common.privacy,
pluginData: this.common.pluginData,
previewfile: this.common.previewfile,
videoPassword: this.common.videoPasswords && this.common.videoPasswords.length !== 0
? this.common.videoPasswords[0]
: null,
downloadEnabled: this.common.downloadEnabled,
originallyPublishedAt: this.common.originallyPublishedAt
? new Date(this.common.originallyPublishedAt)
: null,
schedulePublicationAt: undefined
}
// Special case if we scheduled an update
if (this.common.scheduleUpdate) {
Object.assign(json, {
privacy: VideoEdit.SPECIAL_SCHEDULED_PRIVACY,
schedulePublicationAt: new Date(this.common.scheduleUpdate.updateAt.toString())
})
}
return json
}
toVideoUpdate (): Required<Omit<VideoUpdate, 'commentsEnabled'>> {
return {
...this.toVideoCreateOrUpdate(),
pluginData: this.common.pluginData
}
}
toVideoCreate (overriddenPrivacy: VideoPrivacyType): Required<Omit<VideoCreate, 'commentsEnabled' | 'generateTranscription'>> {
return {
...this.toVideoCreateOrUpdate(),
privacy: overriddenPrivacy
}
}
private toVideoCreateOrUpdate (): Required<Omit<SharedUnionFieldsDeep<VideoCreate | VideoUpdate>, 'commentsEnabled'>> {
return {
name: this.common.name,
category: this.common.category || null,
licence: this.common.licence || null,
language: this.common.language || null,
support: this.common.support || null,
description: this.common.description || null,
channelId: this.common.channelId,
privacy: this.common.privacy,
videoPasswords: this.common.privacy === VideoPrivacy.PASSWORD_PROTECTED
? this.common.videoPasswords
: undefined,
tags: this.common.tags,
nsfw: this.common.nsfw,
nsfwFlags: this.common.nsfwFlags,
nsfwSummary: this.common.nsfwSummary || null,
waitTranscoding: this.common.waitTranscoding,
commentsPolicy: this.common.commentsPolicy,
downloadEnabled: this.common.downloadEnabled,
thumbnailfile: this.common.previewfile,
previewfile: this.common.previewfile,
scheduleUpdate: this.common.scheduleUpdate || null,
originallyPublishedAt: this.common.originallyPublishedAt || null
}
}
// ---------------------------------------------------------------------------
loadFromLiveForm (values: LiveUpdateForm) {
if (values.permanentLive !== undefined) this.live.permanentLive = values.permanentLive
if (values.latencyMode !== undefined) this.live.latencyMode = values.latencyMode
if (values.saveReplay !== undefined) this.live.saveReplay = values.saveReplay
if (values.replayPrivacy !== undefined) {
this.live.replaySettings = values.replayPrivacy
? { privacy: values.replayPrivacy }
: undefined
}
if (values.schedules !== undefined) {
if (values.schedules === null || values.schedules.length === 0 || !values.schedules[0].startAt) {
this.live.schedules = []
} else {
this.live.schedules = values.schedules.map(s => ({
startAt: new Date(s.startAt).toISOString()
}))
}
}
this.updateAfterChange()
}
toLiveFormPatch (): Required<LiveUpdateForm> {
return {
liveStreamKey: this.metadata.live.streamKey,
permanentLive: this.live.permanentLive,
latencyMode: this.live.latencyMode,
saveReplay: this.live.saveReplay,
replayPrivacy: this.live.replaySettings
? this.live.replaySettings.privacy
: VideoPrivacy.PRIVATE,
schedules: this.live.schedules?.map(s => ({
startAt: new Date(s.startAt)
}))
}
}
toLiveUpdate (): LiveVideoUpdate {
return {
permanentLive: this.live.permanentLive,
saveReplay: this.live.saveReplay,
replaySettings: this.live.saveReplay
? this.live.replaySettings
: undefined,
latencyMode: this.live.latencyMode,
schedules: this.live.schedules
}
}
toLiveCreate (overriddenPrivacy: VideoPrivacyType): LiveVideoCreate {
return {
...this.toVideoCreate(overriddenPrivacy),
permanentLive: this.live.permanentLive,
latencyMode: this.live.latencyMode,
saveReplay: this.live.saveReplay,
replaySettings: this.live.replaySettings,
schedules: this.live.schedules
}
}
// ---------------------------------------------------------------------------
toVideoImportCreate (overriddenPrivacy: VideoPrivacyType): VideoImportCreate {
const base: VideoImportCreate = this.toVideoCreate(overriddenPrivacy)
if (this.videoImport.targetUrl) base.targetUrl = this.videoImport.targetUrl
if (this.videoImport.magnetUri) base.magnetUri = this.videoImport.magnetUri
if (this.videoImport.torrentfile) base.torrentfile = this.videoImport.torrentfile
return base
}
// ---------------------------------------------------------------------------
loadFromReplaceFileForm (values: ReplaceFileForm) {
this.replaceFile = values.replaceFile
this.updateAfterChange()
}
toReplaceFileFormPatch (): Required<ReplaceFileForm> {
return { replaceFile: this.replaceFile }
}
resetReplaceFile () {
this.replaceFile = undefined
}
// ---------------------------------------------------------------------------
loadFromStudioForm (values: StudioForm) {
const duration = this.getVideoAttributes().duration
this.studioTasks = []
const cut = values.cut
if ((exists(cut.start) && cut.start !== 0) || (exists(cut.end) && cut.end !== duration)) {
const options: VideoStudioTaskCut['options'] = {}
if (exists(cut.start) && cut.start !== 0) options.start = cut.start
if (exists(cut.end) && cut.end !== duration) options.end = cut.end
this.studioTasks.push({ name: 'cut', options })
}
if (values['add-intro']?.['file']) {
this.studioTasks.push({
name: 'add-intro',
options: {
file: values['add-intro']['file']
}
})
}
if (values['add-outro']?.['file']) {
this.studioTasks.push({
name: 'add-outro',
options: {
file: values['add-outro']['file']
}
})
}
if (values['add-watermark']?.['file']) {
this.studioTasks.push({
name: 'add-watermark',
options: {
file: values['add-watermark']['file']
}
})
}
}
toStudioFormPatch (): Required<StudioForm> {
const cut = this.studioTasks.find(t => t.name === 'cut')
const addIntro = this.studioTasks.find(t => t.name === 'add-intro')
const addOutro = this.studioTasks.find(t => t.name === 'add-outro')
const addWatermark = this.studioTasks.find(t => t.name === 'add-watermark')
return {
'cut': {
start: cut?.options?.start ?? 0,
end: cut?.options?.end ?? this.metadata.duration
},
'add-intro': { file: addIntro?.options?.file as File ?? null },
'add-outro': { file: addOutro?.options?.file as File },
'add-watermark': { file: addWatermark?.options?.file as File }
}
}
resetStudio () {
this.studioTasks = []
}
// ---------------------------------------------------------------------------
loadFromPlayerSettingsForm (values: PlayerSettingsForm) {
this.playerSettings = values
}
toPlayerSettingsFormPatch (): Required<PlayerSettingsForm> {
return {
theme: this.playerSettings?.theme ?? 'channel-default'
}
}
toPlayerSettingsUpdate (): PlayerVideoSettingsUpdate {
if (!this.playerSettings) return undefined
return {
theme: this.playerSettings.theme
}
}
// ---------------------------------------------------------------------------
getVideoSource () {
return this.metadata.videoSource
}
getLive () {
return this.metadata.live
}
getVideoAttributes () {
return this.videoAttributes
}
getChaptersEdit () {
return this.chapters
}
getReplaceFile (): File {
return this.replaceFile
}
getCaptionsEdit () {
return this.captions
}
getStudioTasks () {
return this.studioTasks
}
getPlayerSettings () {
return this.playerSettings
}
getStudioTasksSummary () {
return this.getStudioTasks().map(t => {
if (t.name === 'add-intro') {
return $localize`"${(t.options.file as File).name}" will be added at the beginning of the video`
}
if (t.name === 'add-outro') {
return $localize`"${(t.options.file as File).name}" will be added at the end of the video`
}
if (t.name === 'add-watermark') {
return $localize`"${(t.options.file as File).name}" image watermark will be added to the video`
}
if (t.name === 'cut') {
const { start, end } = t.options
if (start !== undefined && end !== undefined) {
return $localize`Video will begin at ${secondsToTime(start)} and stop at ${secondsToTime(end)}`
}
if (start !== undefined) {
return $localize`Video will begin at ${secondsToTime(start)}`
}
if (end !== undefined) {
return $localize`Video will stop at ${secondsToTime(end)}`
}
}
return ''
})
}
// ---------------------------------------------------------------------------
hasCommonChanges () {
if (this.isNewVideo) return true
if (!this.saveStore.common) return true
let changes = !this.areSameObjects(omit(this.common, [ 'previewfile', 'pluginData' ]), this.saveStore.common)
// Compare preview file
if (changes !== true && (this.common.previewfile || this.saveStore.previewfile)) {
changes = this.common.previewfile?.size !== this.saveStore.previewfile?.size
}
debugLogger('Check if has common changes', {
changes,
common: this.common,
saveCommon: this.saveStore.common,
savePreview: this.saveStore.previewfile
})
return changes
}
hasPluginDataChanges () {
if (!this.checkPluginChanges) return false
if (!this.saveStore.pluginData) return true
const current = this.common.pluginData
const changes = !this.areSameObjects(current, this.saveStore.pluginData)
debugLogger('Check if has plugin data changes', {
changes,
pluginData: current,
savePluginData: this.saveStore.pluginData
})
return changes
}
hasCaptionChanges () {
const changes = this.captions.some(caption => !!caption.action)
debugLogger('Check if caption has changes', { captions: this.captions, changes })
return changes
}
hasChaptersChanges () {
const changes = this.chapters.hasChanges()
debugLogger('Check if chapters has changes', { chapters: this.chapters, changes })
return changes
}
hasLiveChanges () {
if (!this.live) return false
if (!this.saveStore.live) return true
const changes = !this.areSameObjects(this.live, this.saveStore.live)
debugLogger('Check if live has changes', { live: this.live, saveLive: this.saveStore.live, changes })
return changes
}
hasReplaceFile () {
const changes = !!this.replaceFile
debugLogger('Check if replace file has changes', { replaceFile: this.replaceFile, changes })
return changes
}
hasStudioTasks () {
const changes = this.studioTasks.length !== 0
debugLogger('Check if studio has changes', { studioTasks: this.studioTasks, changes })
return changes
}
hasPlayerSettingsChanges () {
if (!this.playerSettings) return false
if (!this.saveStore.playerSettings) return true
const changes = !this.areSameObjects(this.playerSettings, this.saveStore.playerSettings)
debugLogger('Check if player settings has changes', {
playerSettings: this.playerSettings,
savePlayerSettings: this.saveStore.playerSettings,
changes
})
return changes
}
// ---------------------------------------------------------------------------
hasPendingChanges () {
return this.hasCaptionChanges() ||
this.hasLiveChanges() ||
this.hasReplaceFile() ||
this.hasStudioTasks() ||
this.hasChaptersChanges() ||
this.hasCommonChanges() ||
this.hasPluginDataChanges() ||
this.hasPlayerSettingsChanges()
}
// ---------------------------------------------------------------------------
isPublishedVOD () {
return !this.metadata.isLive && this.metadata.state === VideoState.PUBLISHED
}
private updateAfterChange () {
this.videoAttributes = {
id: this.metadata.id,
shortUUID: this.metadata.shortUUID,
uuid: this.metadata.uuid,
name: this.common.name,
state: this.metadata.state,
privacy: this.common.privacy,
isLive: this.metadata.isLive,
aspectRatio: this.metadata.aspectRatio,
views: this.metadata.views,
likes: this.metadata.likes,
duration: this.metadata.duration,
blacklisted: this.metadata.blacklisted,
live: this.metadata.live
}
}
// ---------------------------------------------------------------------------
areSameObjects<T, U> (a: Jsonify<T>, b: Jsonify<U>) {
// Allow '' === null
if (typeof a === 'string') return a === b || !a && b === null
if (typeof b === 'string') return a === b || !b && a === null
// Allow null === undefined
if (a === undefined) return !exists(b)
if (b === undefined) return !exists(a)
if (a as any === b as any) return true
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) {
return false
}
const keysA = Object.keys(a)
const keysB = Object.keys(b)
if (keysA.length !== keysB.length) return false
for (const key of keysA) {
if (!keysB.includes(key)) return false
if (!this.areSameObjects((a as any)[key], (b as any)[key])) return false
}
return true
}
// ---------------------------------------------------------------------------
onSave () {
this.isNewVideo = false
}
enableCheckPluginChanges () {
this.checkPluginChanges = true
}
disableCheckPluginChanges () {
this.checkPluginChanges = false
}
}