1 import { Transaction } from 'sequelize/types'
2 import { resetSequelizeInstance } from '@server/helpers/database-utils'
3 import { logger, loggerTagsFactory, LoggerTagsFn } from '@server/helpers/logger'
4 import { sequelizeTypescript } from '@server/initializers/database'
5 import { Notifier } from '@server/lib/notifier'
6 import { PeerTubeSocket } from '@server/lib/peertube-socket'
7 import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist'
8 import { VideoCaptionModel } from '@server/models/video/video-caption'
9 import { VideoLiveModel } from '@server/models/video/video-live'
10 import { MActor, MChannelAccountLight, MChannelId, MVideoAccountLightBlacklistAllFiles, MVideoFullLight } from '@server/types/models'
11 import { VideoObject, VideoPrivacy } from '@shared/models'
12 import { APVideoAbstractBuilder, getVideoAttributesFromObject } from './shared'
14 export class APVideoUpdater extends APVideoAbstractBuilder {
15 private readonly wasPrivateVideo: boolean
16 private readonly wasUnlistedVideo: boolean
18 private readonly videoFieldsSave: any
20 private readonly oldVideoChannel: MChannelAccountLight
22 protected lTags: LoggerTagsFn
25 protected readonly videoObject: VideoObject,
26 private readonly video: MVideoAccountLightBlacklistAllFiles
30 this.wasPrivateVideo = this.video.privacy === VideoPrivacy.PRIVATE
31 this.wasUnlistedVideo = this.video.privacy === VideoPrivacy.UNLISTED
33 this.oldVideoChannel = this.video.VideoChannel
35 this.videoFieldsSave = this.video.toJSON()
37 this.lTags = loggerTagsFactory('ap', 'video', 'update', video.uuid, video.url)
40 async update (overrideTo?: string[]) {
42 'Updating remote video "%s".', this.videoObject.uuid,
43 { videoObject: this.videoObject, ...this.lTags() }
47 const channelActor = await this.getOrCreateVideoChannelFromVideoObject()
49 const thumbnailModel = await this.tryToGenerateThumbnail(this.video)
51 const videoUpdated = await sequelizeTypescript.transaction(async t => {
52 this.checkChannelUpdateOrThrow(channelActor)
54 const videoUpdated = await this.updateVideo(channelActor.VideoChannel, t, overrideTo)
56 if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t)
58 await this.setPreview(videoUpdated, t)
59 await this.setWebTorrentFiles(videoUpdated, t)
60 await this.setStreamingPlaylists(videoUpdated, t)
61 await this.setTags(videoUpdated, t)
62 await this.setTrackers(videoUpdated, t)
63 await this.setCaptions(videoUpdated, t)
64 await this.setOrDeleteLive(videoUpdated, t)
69 await autoBlacklistVideoIfNeeded({
74 transaction: undefined
78 if (this.wasPrivateVideo || this.wasUnlistedVideo) {
79 Notifier.Instance.notifyOnNewVideoIfNeeded(videoUpdated)
82 if (videoUpdated.isLive) {
83 PeerTubeSocket.Instance.sendVideoLiveNewState(videoUpdated)
84 PeerTubeSocket.Instance.sendVideoViewsUpdate(videoUpdated)
87 logger.info('Remote video with uuid %s updated', this.videoObject.uuid, this.lTags())
91 this.catchUpdateError(err)
95 // Check we can update the channel: we trust the remote server
96 private checkChannelUpdateOrThrow (newChannelActor: MActor) {
97 if (!this.oldVideoChannel.Actor.serverId || !newChannelActor.serverId) {
98 throw new Error('Cannot check old channel/new channel validity because `serverId` is null')
101 if (this.oldVideoChannel.Actor.serverId !== newChannelActor.serverId) {
102 throw new Error(`New channel ${newChannelActor.url} is not on the same server than new channel ${this.oldVideoChannel.Actor.url}`)
106 private updateVideo (channel: MChannelId, transaction: Transaction, overrideTo?: string[]) {
107 const to = overrideTo || this.videoObject.to
108 const videoData = getVideoAttributesFromObject(channel, this.videoObject, to)
109 this.video.name = videoData.name
110 this.video.uuid = videoData.uuid
111 this.video.url = videoData.url
112 this.video.category = videoData.category
113 this.video.licence = videoData.licence
114 this.video.language = videoData.language
115 this.video.description = videoData.description
116 this.video.support = videoData.support
117 this.video.nsfw = videoData.nsfw
118 this.video.commentsEnabled = videoData.commentsEnabled
119 this.video.downloadEnabled = videoData.downloadEnabled
120 this.video.waitTranscoding = videoData.waitTranscoding
121 this.video.state = videoData.state
122 this.video.duration = videoData.duration
123 this.video.createdAt = videoData.createdAt
124 this.video.publishedAt = videoData.publishedAt
125 this.video.originallyPublishedAt = videoData.originallyPublishedAt
126 this.video.privacy = videoData.privacy
127 this.video.channelId = videoData.channelId
128 this.video.views = videoData.views
129 this.video.isLive = videoData.isLive
131 // Ensures we update the updatedAt attribute, even if main attributes did not change
132 this.video.changed('updatedAt', true)
134 return this.video.save({ transaction }) as Promise<MVideoFullLight>
137 private async setCaptions (videoUpdated: MVideoFullLight, t: Transaction) {
138 await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(videoUpdated.id, t)
140 await this.insertOrReplaceCaptions(videoUpdated, t)
143 private async setOrDeleteLive (videoUpdated: MVideoFullLight, transaction: Transaction) {
144 if (this.video.isLive) return this.insertOrReplaceLive(videoUpdated, transaction)
146 // Delete existing live if it exists
147 await VideoLiveModel.destroy({
149 videoId: this.video.id
154 videoUpdated.VideoLive = null
157 private catchUpdateError (err: Error) {
158 if (this.video !== undefined && this.videoFieldsSave !== undefined) {
159 resetSequelizeInstance(this.video, this.videoFieldsSave)
162 // This is just a debug because we will retry the insert
163 logger.debug('Cannot update the remote video.', { err, ...this.lTags() })