aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/activitypub/videos
diff options
context:
space:
mode:
Diffstat (limited to 'server/lib/activitypub/videos')
-rw-r--r--server/lib/activitypub/videos/federate.ts29
-rw-r--r--server/lib/activitypub/videos/get.ts116
-rw-r--r--server/lib/activitypub/videos/index.ts4
-rw-r--r--server/lib/activitypub/videos/refresh.ts68
-rw-r--r--server/lib/activitypub/videos/shared/abstract-builder.ts190
-rw-r--r--server/lib/activitypub/videos/shared/creator.ts65
-rw-r--r--server/lib/activitypub/videos/shared/index.ts6
-rw-r--r--server/lib/activitypub/videos/shared/object-to-model-attributes.ts285
-rw-r--r--server/lib/activitypub/videos/shared/trackers.ts43
-rw-r--r--server/lib/activitypub/videos/shared/url-to-object.ts25
-rw-r--r--server/lib/activitypub/videos/shared/video-sync-attributes.ts107
-rw-r--r--server/lib/activitypub/videos/updater.ts180
12 files changed, 0 insertions, 1118 deletions
diff --git a/server/lib/activitypub/videos/federate.ts b/server/lib/activitypub/videos/federate.ts
deleted file mode 100644
index d7e251153..000000000
--- a/server/lib/activitypub/videos/federate.ts
+++ /dev/null
@@ -1,29 +0,0 @@
1import { Transaction } from 'sequelize/types'
2import { MVideoAP, MVideoAPLight } from '@server/types/models'
3import { sendCreateVideo, sendUpdateVideo } from '../send'
4import { shareVideoByServerAndChannel } from '../share'
5
6async function federateVideoIfNeeded (videoArg: MVideoAPLight, isNewVideo: boolean, transaction?: Transaction) {
7 const video = videoArg as MVideoAP
8
9 if (
10 // Check this is not a blacklisted video, or unfederated blacklisted video
11 (video.isBlacklisted() === false || (isNewVideo === false && video.VideoBlacklist.unfederated === false)) &&
12 // Check the video is public/unlisted and published
13 video.hasPrivacyForFederation() && video.hasStateForFederation()
14 ) {
15 const video = await videoArg.lightAPToFullAP(transaction)
16
17 if (isNewVideo) {
18 // Now we'll add the video's meta data to our followers
19 await sendCreateVideo(video, transaction)
20 await shareVideoByServerAndChannel(video, transaction)
21 } else {
22 await sendUpdateVideo(video, transaction)
23 }
24 }
25}
26
27export {
28 federateVideoIfNeeded
29}
diff --git a/server/lib/activitypub/videos/get.ts b/server/lib/activitypub/videos/get.ts
deleted file mode 100644
index 288c506ee..000000000
--- a/server/lib/activitypub/videos/get.ts
+++ /dev/null
@@ -1,116 +0,0 @@
1import { retryTransactionWrapper } from '@server/helpers/database-utils'
2import { logger } from '@server/helpers/logger'
3import { JobQueue } from '@server/lib/job-queue'
4import { loadVideoByUrl, VideoLoadByUrlType } from '@server/lib/model-loaders'
5import { MVideoAccountLightBlacklistAllFiles, MVideoImmutable, MVideoThumbnail } from '@server/types/models'
6import { APObjectId } from '@shared/models'
7import { getAPId } from '../activity'
8import { refreshVideoIfNeeded } from './refresh'
9import { APVideoCreator, fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared'
10
11type GetVideoResult <T> = Promise<{
12 video: T
13 created: boolean
14 autoBlacklisted?: boolean
15}>
16
17type GetVideoParamAll = {
18 videoObject: APObjectId
19 syncParam?: SyncParam
20 fetchType?: 'all'
21 allowRefresh?: boolean
22}
23
24type GetVideoParamImmutable = {
25 videoObject: APObjectId
26 syncParam?: SyncParam
27 fetchType: 'only-immutable-attributes'
28 allowRefresh: false
29}
30
31type GetVideoParamOther = {
32 videoObject: APObjectId
33 syncParam?: SyncParam
34 fetchType?: 'all' | 'only-video'
35 allowRefresh?: boolean
36}
37
38function getOrCreateAPVideo (options: GetVideoParamAll): GetVideoResult<MVideoAccountLightBlacklistAllFiles>
39function getOrCreateAPVideo (options: GetVideoParamImmutable): GetVideoResult<MVideoImmutable>
40function getOrCreateAPVideo (options: GetVideoParamOther): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail>
41
42async function getOrCreateAPVideo (
43 options: GetVideoParamAll | GetVideoParamImmutable | GetVideoParamOther
44): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> {
45 // Default params
46 const syncParam = options.syncParam || { rates: true, shares: true, comments: true, refreshVideo: false }
47 const fetchType = options.fetchType || 'all'
48 const allowRefresh = options.allowRefresh !== false
49
50 // Get video url
51 const videoUrl = getAPId(options.videoObject)
52 let videoFromDatabase = await loadVideoByUrl(videoUrl, fetchType)
53
54 if (videoFromDatabase) {
55 if (allowRefresh === true) {
56 // Typings ensure allowRefresh === false in only-immutable-attributes fetch type
57 videoFromDatabase = await scheduleRefresh(videoFromDatabase as MVideoThumbnail, fetchType, syncParam)
58 }
59
60 return { video: videoFromDatabase, created: false }
61 }
62
63 const { videoObject } = await fetchRemoteVideo(videoUrl)
64 if (!videoObject) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
65
66 // videoUrl is just an alias/rediraction, so process object id instead
67 if (videoObject.id !== videoUrl) return getOrCreateAPVideo({ ...options, fetchType: 'all', videoObject })
68
69 try {
70 const creator = new APVideoCreator(videoObject)
71 const { autoBlacklisted, videoCreated } = await retryTransactionWrapper(creator.create.bind(creator))
72
73 await syncVideoExternalAttributes(videoCreated, videoObject, syncParam)
74
75 return { video: videoCreated, created: true, autoBlacklisted }
76 } catch (err) {
77 // Maybe a concurrent getOrCreateAPVideo call created this video
78 if (err.name === 'SequelizeUniqueConstraintError') {
79 const alreadyCreatedVideo = await loadVideoByUrl(videoUrl, fetchType)
80 if (alreadyCreatedVideo) return { video: alreadyCreatedVideo, created: false }
81
82 logger.error('Cannot create video %s because of SequelizeUniqueConstraintError error, but cannot find it in database.', videoUrl)
83 }
84
85 throw err
86 }
87}
88
89// ---------------------------------------------------------------------------
90
91export {
92 getOrCreateAPVideo
93}
94
95// ---------------------------------------------------------------------------
96
97async function scheduleRefresh (video: MVideoThumbnail, fetchType: VideoLoadByUrlType, syncParam: SyncParam) {
98 if (!video.isOutdated()) return video
99
100 const refreshOptions = {
101 video,
102 fetchedType: fetchType,
103 syncParam
104 }
105
106 if (syncParam.refreshVideo === true) {
107 return refreshVideoIfNeeded(refreshOptions)
108 }
109
110 await JobQueue.Instance.createJob({
111 type: 'activitypub-refresher',
112 payload: { type: 'video', url: video.url }
113 })
114
115 return video
116}
diff --git a/server/lib/activitypub/videos/index.ts b/server/lib/activitypub/videos/index.ts
deleted file mode 100644
index b22062598..000000000
--- a/server/lib/activitypub/videos/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
1export * from './federate'
2export * from './get'
3export * from './refresh'
4export * from './updater'
diff --git a/server/lib/activitypub/videos/refresh.ts b/server/lib/activitypub/videos/refresh.ts
deleted file mode 100644
index 9f952a218..000000000
--- a/server/lib/activitypub/videos/refresh.ts
+++ /dev/null
@@ -1,68 +0,0 @@
1import { logger, loggerTagsFactory } from '@server/helpers/logger'
2import { PeerTubeRequestError } from '@server/helpers/requests'
3import { VideoLoadByUrlType } from '@server/lib/model-loaders'
4import { VideoModel } from '@server/models/video/video'
5import { MVideoAccountLightBlacklistAllFiles, MVideoThumbnail } from '@server/types/models'
6import { HttpStatusCode } from '@shared/models'
7import { ActorFollowHealthCache } from '../../actor-follow-health-cache'
8import { fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared'
9import { APVideoUpdater } from './updater'
10
11async function refreshVideoIfNeeded (options: {
12 video: MVideoThumbnail
13 fetchedType: VideoLoadByUrlType
14 syncParam: SyncParam
15}): Promise<MVideoThumbnail> {
16 if (!options.video.isOutdated()) return options.video
17
18 // We need more attributes if the argument video was fetched with not enough joints
19 const video = options.fetchedType === 'all'
20 ? options.video as MVideoAccountLightBlacklistAllFiles
21 : await VideoModel.loadByUrlAndPopulateAccount(options.video.url)
22
23 const lTags = loggerTagsFactory('ap', 'video', 'refresh', video.uuid, video.url)
24
25 logger.info('Refreshing video %s.', video.url, lTags())
26
27 try {
28 const { videoObject } = await fetchRemoteVideo(video.url)
29
30 if (videoObject === undefined) {
31 logger.warn('Cannot refresh remote video %s: invalid body.', video.url, lTags())
32
33 await video.setAsRefreshed()
34 return video
35 }
36
37 const videoUpdater = new APVideoUpdater(videoObject, video)
38 await videoUpdater.update()
39
40 await syncVideoExternalAttributes(video, videoObject, options.syncParam)
41
42 ActorFollowHealthCache.Instance.addGoodServerId(video.VideoChannel.Actor.serverId)
43
44 return video
45 } catch (err) {
46 if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) {
47 logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url, lTags())
48
49 // Video does not exist anymore
50 await video.destroy()
51 return undefined
52 }
53
54 logger.warn('Cannot refresh video %s.', options.video.url, { err, ...lTags() })
55
56 ActorFollowHealthCache.Instance.addBadServerId(video.VideoChannel.Actor.serverId)
57
58 // Don't refresh in loop
59 await video.setAsRefreshed()
60 return video
61 }
62}
63
64// ---------------------------------------------------------------------------
65
66export {
67 refreshVideoIfNeeded
68}
diff --git a/server/lib/activitypub/videos/shared/abstract-builder.ts b/server/lib/activitypub/videos/shared/abstract-builder.ts
deleted file mode 100644
index 98c2f58eb..000000000
--- a/server/lib/activitypub/videos/shared/abstract-builder.ts
+++ /dev/null
@@ -1,190 +0,0 @@
1import { CreationAttributes, Transaction } from 'sequelize/types'
2import { deleteAllModels, filterNonExistingModels } from '@server/helpers/database-utils'
3import { logger, LoggerTagsFn } from '@server/helpers/logger'
4import { updateRemoteVideoThumbnail } from '@server/lib/thumbnail'
5import { setVideoTags } from '@server/lib/video'
6import { StoryboardModel } from '@server/models/video/storyboard'
7import { VideoCaptionModel } from '@server/models/video/video-caption'
8import { VideoFileModel } from '@server/models/video/video-file'
9import { VideoLiveModel } from '@server/models/video/video-live'
10import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
11import {
12 MStreamingPlaylistFiles,
13 MStreamingPlaylistFilesVideo,
14 MVideoCaption,
15 MVideoFile,
16 MVideoFullLight,
17 MVideoThumbnail
18} from '@server/types/models'
19import { ActivityTagObject, ThumbnailType, VideoObject, VideoStreamingPlaylistType } from '@shared/models'
20import { findOwner, getOrCreateAPActor } from '../../actors'
21import {
22 getCaptionAttributesFromObject,
23 getFileAttributesFromUrl,
24 getLiveAttributesFromObject,
25 getPreviewFromIcons,
26 getStoryboardAttributeFromObject,
27 getStreamingPlaylistAttributesFromObject,
28 getTagsFromObject,
29 getThumbnailFromIcons
30} from './object-to-model-attributes'
31import { getTrackerUrls, setVideoTrackers } from './trackers'
32
33export abstract class APVideoAbstractBuilder {
34 protected abstract videoObject: VideoObject
35 protected abstract lTags: LoggerTagsFn
36
37 protected async getOrCreateVideoChannelFromVideoObject () {
38 const channel = await findOwner(this.videoObject.id, this.videoObject.attributedTo, 'Group')
39 if (!channel) throw new Error('Cannot find associated video channel to video ' + this.videoObject.url)
40
41 return getOrCreateAPActor(channel.id, 'all')
42 }
43
44 protected async setThumbnail (video: MVideoThumbnail, t?: Transaction) {
45 const miniatureIcon = getThumbnailFromIcons(this.videoObject)
46 if (!miniatureIcon) {
47 logger.warn('Cannot find thumbnail in video object', { object: this.videoObject })
48 return undefined
49 }
50
51 const miniatureModel = updateRemoteVideoThumbnail({
52 fileUrl: miniatureIcon.url,
53 video,
54 type: ThumbnailType.MINIATURE,
55 size: miniatureIcon,
56 onDisk: false // Lazy download remote thumbnails
57 })
58
59 await video.addAndSaveThumbnail(miniatureModel, t)
60 }
61
62 protected async setPreview (video: MVideoFullLight, t?: Transaction) {
63 const previewIcon = getPreviewFromIcons(this.videoObject)
64 if (!previewIcon) return
65
66 const previewModel = updateRemoteVideoThumbnail({
67 fileUrl: previewIcon.url,
68 video,
69 type: ThumbnailType.PREVIEW,
70 size: previewIcon,
71 onDisk: false // Lazy download remote previews
72 })
73
74 await video.addAndSaveThumbnail(previewModel, t)
75 }
76
77 protected async setTags (video: MVideoFullLight, t: Transaction) {
78 const tags = getTagsFromObject(this.videoObject)
79 await setVideoTags({ video, tags, transaction: t })
80 }
81
82 protected async setTrackers (video: MVideoFullLight, t: Transaction) {
83 const trackers = getTrackerUrls(this.videoObject, video)
84 await setVideoTrackers({ video, trackers, transaction: t })
85 }
86
87 protected async insertOrReplaceCaptions (video: MVideoFullLight, t: Transaction) {
88 const existingCaptions = await VideoCaptionModel.listVideoCaptions(video.id, t)
89
90 let captionsToCreate = getCaptionAttributesFromObject(video, this.videoObject)
91 .map(a => new VideoCaptionModel(a) as MVideoCaption)
92
93 for (const existingCaption of existingCaptions) {
94 // Only keep captions that do not already exist
95 const filtered = captionsToCreate.filter(c => !c.isEqual(existingCaption))
96
97 // This caption already exists, we don't need to destroy and create it
98 if (filtered.length !== captionsToCreate.length) {
99 captionsToCreate = filtered
100 continue
101 }
102
103 // Destroy this caption that does not exist anymore
104 await existingCaption.destroy({ transaction: t })
105 }
106
107 for (const captionToCreate of captionsToCreate) {
108 await captionToCreate.save({ transaction: t })
109 }
110 }
111
112 protected async insertOrReplaceStoryboard (video: MVideoFullLight, t: Transaction) {
113 const existingStoryboard = await StoryboardModel.loadByVideo(video.id, t)
114 if (existingStoryboard) await existingStoryboard.destroy({ transaction: t })
115
116 const storyboardAttributes = getStoryboardAttributeFromObject(video, this.videoObject)
117 if (!storyboardAttributes) return
118
119 return StoryboardModel.create(storyboardAttributes, { transaction: t })
120 }
121
122 protected async insertOrReplaceLive (video: MVideoFullLight, transaction: Transaction) {
123 const attributes = getLiveAttributesFromObject(video, this.videoObject)
124 const [ videoLive ] = await VideoLiveModel.upsert(attributes, { transaction, returning: true })
125
126 video.VideoLive = videoLive
127 }
128
129 protected async setWebVideoFiles (video: MVideoFullLight, t: Transaction) {
130 const videoFileAttributes = getFileAttributesFromUrl(video, this.videoObject.url)
131 const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a))
132
133 // Remove video files that do not exist anymore
134 await deleteAllModels(filterNonExistingModels(video.VideoFiles || [], newVideoFiles), t)
135
136 // Update or add other one
137 const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'video', t))
138 video.VideoFiles = await Promise.all(upsertTasks)
139 }
140
141 protected async setStreamingPlaylists (video: MVideoFullLight, t: Transaction) {
142 const streamingPlaylistAttributes = getStreamingPlaylistAttributesFromObject(video, this.videoObject)
143 const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a))
144
145 // Remove video playlists that do not exist anymore
146 await deleteAllModels(filterNonExistingModels(video.VideoStreamingPlaylists || [], newStreamingPlaylists), t)
147
148 const oldPlaylists = video.VideoStreamingPlaylists
149 video.VideoStreamingPlaylists = []
150
151 for (const playlistAttributes of streamingPlaylistAttributes) {
152 const streamingPlaylistModel = await this.insertOrReplaceStreamingPlaylist(playlistAttributes, t)
153 streamingPlaylistModel.Video = video
154
155 await this.setStreamingPlaylistFiles(oldPlaylists, streamingPlaylistModel, playlistAttributes.tagAPObject, t)
156
157 video.VideoStreamingPlaylists.push(streamingPlaylistModel)
158 }
159 }
160
161 private async insertOrReplaceStreamingPlaylist (attributes: CreationAttributes<VideoStreamingPlaylistModel>, t: Transaction) {
162 const [ streamingPlaylist ] = await VideoStreamingPlaylistModel.upsert(attributes, { returning: true, transaction: t })
163
164 return streamingPlaylist as MStreamingPlaylistFilesVideo
165 }
166
167 private getStreamingPlaylistFiles (oldPlaylists: MStreamingPlaylistFiles[], type: VideoStreamingPlaylistType) {
168 const playlist = oldPlaylists.find(s => s.type === type)
169 if (!playlist) return []
170
171 return playlist.VideoFiles
172 }
173
174 private async setStreamingPlaylistFiles (
175 oldPlaylists: MStreamingPlaylistFiles[],
176 playlistModel: MStreamingPlaylistFilesVideo,
177 tagObjects: ActivityTagObject[],
178 t: Transaction
179 ) {
180 const oldStreamingPlaylistFiles = this.getStreamingPlaylistFiles(oldPlaylists || [], playlistModel.type)
181
182 const newVideoFiles: MVideoFile[] = getFileAttributesFromUrl(playlistModel, tagObjects).map(a => new VideoFileModel(a))
183
184 await deleteAllModels(filterNonExistingModels(oldStreamingPlaylistFiles, newVideoFiles), t)
185
186 // Update or add other one
187 const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'streaming-playlist', t))
188 playlistModel.VideoFiles = await Promise.all(upsertTasks)
189 }
190}
diff --git a/server/lib/activitypub/videos/shared/creator.ts b/server/lib/activitypub/videos/shared/creator.ts
deleted file mode 100644
index e44fd0d52..000000000
--- a/server/lib/activitypub/videos/shared/creator.ts
+++ /dev/null
@@ -1,65 +0,0 @@
1
2import { logger, loggerTagsFactory, LoggerTagsFn } from '@server/helpers/logger'
3import { sequelizeTypescript } from '@server/initializers/database'
4import { Hooks } from '@server/lib/plugins/hooks'
5import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist'
6import { VideoModel } from '@server/models/video/video'
7import { MVideoFullLight, MVideoThumbnail } from '@server/types/models'
8import { VideoObject } from '@shared/models'
9import { APVideoAbstractBuilder } from './abstract-builder'
10import { getVideoAttributesFromObject } from './object-to-model-attributes'
11
12export class APVideoCreator extends APVideoAbstractBuilder {
13 protected lTags: LoggerTagsFn
14
15 constructor (protected readonly videoObject: VideoObject) {
16 super()
17
18 this.lTags = loggerTagsFactory('ap', 'video', 'create', this.videoObject.uuid, this.videoObject.id)
19 }
20
21 async create () {
22 logger.debug('Adding remote video %s.', this.videoObject.id, this.lTags())
23
24 const channelActor = await this.getOrCreateVideoChannelFromVideoObject()
25 const channel = channelActor.VideoChannel
26
27 const videoData = getVideoAttributesFromObject(channel, this.videoObject, this.videoObject.to)
28 const video = VideoModel.build({ ...videoData, likes: 0, dislikes: 0 }) as MVideoThumbnail
29
30 const { autoBlacklisted, videoCreated } = await sequelizeTypescript.transaction(async t => {
31 const videoCreated = await video.save({ transaction: t }) as MVideoFullLight
32 videoCreated.VideoChannel = channel
33
34 await this.setThumbnail(videoCreated, t)
35 await this.setPreview(videoCreated, t)
36 await this.setWebVideoFiles(videoCreated, t)
37 await this.setStreamingPlaylists(videoCreated, t)
38 await this.setTags(videoCreated, t)
39 await this.setTrackers(videoCreated, t)
40 await this.insertOrReplaceCaptions(videoCreated, t)
41 await this.insertOrReplaceLive(videoCreated, t)
42 await this.insertOrReplaceStoryboard(videoCreated, t)
43
44 // We added a video in this channel, set it as updated
45 await channel.setAsUpdated(t)
46
47 const autoBlacklisted = await autoBlacklistVideoIfNeeded({
48 video: videoCreated,
49 user: undefined,
50 isRemote: true,
51 isNew: true,
52 isNewFile: true,
53 transaction: t
54 })
55
56 logger.info('Remote video with uuid %s inserted.', this.videoObject.uuid, this.lTags())
57
58 Hooks.runAction('action:activity-pub.remote-video.created', { video: videoCreated, videoAPObject: this.videoObject })
59
60 return { autoBlacklisted, videoCreated }
61 })
62
63 return { autoBlacklisted, videoCreated }
64 }
65}
diff --git a/server/lib/activitypub/videos/shared/index.ts b/server/lib/activitypub/videos/shared/index.ts
deleted file mode 100644
index 951403493..000000000
--- a/server/lib/activitypub/videos/shared/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
1export * from './abstract-builder'
2export * from './creator'
3export * from './object-to-model-attributes'
4export * from './trackers'
5export * from './url-to-object'
6export * from './video-sync-attributes'
diff --git a/server/lib/activitypub/videos/shared/object-to-model-attributes.ts b/server/lib/activitypub/videos/shared/object-to-model-attributes.ts
deleted file mode 100644
index 6cbe72e27..000000000
--- a/server/lib/activitypub/videos/shared/object-to-model-attributes.ts
+++ /dev/null
@@ -1,285 +0,0 @@
1import { maxBy, minBy } from 'lodash'
2import { decode as magnetUriDecode } from 'magnet-uri'
3import { basename, extname } from 'path'
4import { isAPVideoFileUrlMetadataObject } from '@server/helpers/custom-validators/activitypub/videos'
5import { isVideoFileInfoHashValid } from '@server/helpers/custom-validators/videos'
6import { logger } from '@server/helpers/logger'
7import { getExtFromMimetype } from '@server/helpers/video'
8import { ACTIVITY_PUB, MIMETYPES, P2P_MEDIA_LOADER_PEER_VERSION, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '@server/initializers/constants'
9import { generateTorrentFileName } from '@server/lib/paths'
10import { VideoCaptionModel } from '@server/models/video/video-caption'
11import { VideoFileModel } from '@server/models/video/video-file'
12import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
13import { FilteredModelAttributes } from '@server/types'
14import { isStreamingPlaylist, MChannelId, MStreamingPlaylistVideo, MVideo, MVideoId } from '@server/types/models'
15import {
16 ActivityHashTagObject,
17 ActivityMagnetUrlObject,
18 ActivityPlaylistSegmentHashesObject,
19 ActivityPlaylistUrlObject,
20 ActivityTagObject,
21 ActivityUrlObject,
22 ActivityVideoUrlObject,
23 VideoObject,
24 VideoPrivacy,
25 VideoStreamingPlaylistType
26} from '@shared/models'
27import { getDurationFromActivityStream } from '../../activity'
28import { isArray } from '@server/helpers/custom-validators/misc'
29import { generateImageFilename } from '@server/helpers/image-utils'
30import { arrayify } from '@shared/core-utils'
31
32function getThumbnailFromIcons (videoObject: VideoObject) {
33 let validIcons = videoObject.icon.filter(i => i.width > THUMBNAILS_SIZE.minWidth)
34 // Fallback if there are not valid icons
35 if (validIcons.length === 0) validIcons = videoObject.icon
36
37 return minBy(validIcons, 'width')
38}
39
40function getPreviewFromIcons (videoObject: VideoObject) {
41 const validIcons = videoObject.icon.filter(i => i.width > PREVIEWS_SIZE.minWidth)
42
43 return maxBy(validIcons, 'width')
44}
45
46function getTagsFromObject (videoObject: VideoObject) {
47 return videoObject.tag
48 .filter(isAPHashTagObject)
49 .map(t => t.name)
50}
51
52function getFileAttributesFromUrl (
53 videoOrPlaylist: MVideo | MStreamingPlaylistVideo,
54 urls: (ActivityTagObject | ActivityUrlObject)[]
55) {
56 const fileUrls = urls.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
57
58 if (fileUrls.length === 0) return []
59
60 const attributes: FilteredModelAttributes<VideoFileModel>[] = []
61 for (const fileUrl of fileUrls) {
62 // Fetch associated magnet uri
63 const magnet = urls.filter(isAPMagnetUrlObject)
64 .find(u => u.height === fileUrl.height)
65
66 if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.href)
67
68 const parsed = magnetUriDecode(magnet.href)
69 if (!parsed || isVideoFileInfoHashValid(parsed.infoHash) === false) {
70 throw new Error('Cannot parse magnet URI ' + magnet.href)
71 }
72
73 const torrentUrl = Array.isArray(parsed.xs)
74 ? parsed.xs[0]
75 : parsed.xs
76
77 // Fetch associated metadata url, if any
78 const metadata = urls.filter(isAPVideoFileUrlMetadataObject)
79 .find(u => {
80 return u.height === fileUrl.height &&
81 u.fps === fileUrl.fps &&
82 u.rel.includes(fileUrl.mediaType)
83 })
84
85 const extname = getExtFromMimetype(MIMETYPES.VIDEO.MIMETYPE_EXT, fileUrl.mediaType)
86 const resolution = fileUrl.height
87 const videoId = isStreamingPlaylist(videoOrPlaylist) ? null : videoOrPlaylist.id
88 const videoStreamingPlaylistId = isStreamingPlaylist(videoOrPlaylist) ? videoOrPlaylist.id : null
89
90 const attribute = {
91 extname,
92 infoHash: parsed.infoHash,
93 resolution,
94 size: fileUrl.size,
95 fps: fileUrl.fps || -1,
96 metadataUrl: metadata?.href,
97
98 // Use the name of the remote file because we don't proxify video file requests
99 filename: basename(fileUrl.href),
100 fileUrl: fileUrl.href,
101
102 torrentUrl,
103 // Use our own torrent name since we proxify torrent requests
104 torrentFilename: generateTorrentFileName(videoOrPlaylist, resolution),
105
106 // This is a video file owned by a video or by a streaming playlist
107 videoId,
108 videoStreamingPlaylistId
109 }
110
111 attributes.push(attribute)
112 }
113
114 return attributes
115}
116
117function getStreamingPlaylistAttributesFromObject (video: MVideoId, videoObject: VideoObject) {
118 const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[]
119 if (playlistUrls.length === 0) return []
120
121 const attributes: (FilteredModelAttributes<VideoStreamingPlaylistModel> & { tagAPObject?: ActivityTagObject[] })[] = []
122 for (const playlistUrlObject of playlistUrls) {
123 const segmentsSha256UrlObject = playlistUrlObject.tag.find(isAPPlaylistSegmentHashesUrlObject)
124
125 const files: unknown[] = playlistUrlObject.tag.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
126
127 if (!segmentsSha256UrlObject) {
128 logger.warn('No segment sha256 URL found in AP playlist object.', { playlistUrl: playlistUrlObject })
129 continue
130 }
131
132 const attribute = {
133 type: VideoStreamingPlaylistType.HLS,
134
135 playlistFilename: basename(playlistUrlObject.href),
136 playlistUrl: playlistUrlObject.href,
137
138 segmentsSha256Filename: basename(segmentsSha256UrlObject.href),
139 segmentsSha256Url: segmentsSha256UrlObject.href,
140
141 p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrlObject.href, files),
142 p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
143 videoId: video.id,
144
145 tagAPObject: playlistUrlObject.tag
146 }
147
148 attributes.push(attribute)
149 }
150
151 return attributes
152}
153
154function getLiveAttributesFromObject (video: MVideoId, videoObject: VideoObject) {
155 return {
156 saveReplay: videoObject.liveSaveReplay,
157 permanentLive: videoObject.permanentLive,
158 latencyMode: videoObject.latencyMode,
159 videoId: video.id
160 }
161}
162
163function getCaptionAttributesFromObject (video: MVideoId, videoObject: VideoObject) {
164 return videoObject.subtitleLanguage.map(c => ({
165 videoId: video.id,
166 filename: VideoCaptionModel.generateCaptionName(c.identifier),
167 language: c.identifier,
168 fileUrl: c.url
169 }))
170}
171
172function getStoryboardAttributeFromObject (video: MVideoId, videoObject: VideoObject) {
173 if (!isArray(videoObject.preview)) return undefined
174
175 const storyboard = videoObject.preview.find(p => p.rel.includes('storyboard'))
176 if (!storyboard) return undefined
177
178 const url = arrayify(storyboard.url).find(u => u.mediaType === 'image/jpeg')
179
180 return {
181 filename: generateImageFilename(extname(url.href)),
182 totalHeight: url.height,
183 totalWidth: url.width,
184 spriteHeight: url.tileHeight,
185 spriteWidth: url.tileWidth,
186 spriteDuration: getDurationFromActivityStream(url.tileDuration),
187 fileUrl: url.href,
188 videoId: video.id
189 }
190}
191
192function getVideoAttributesFromObject (videoChannel: MChannelId, videoObject: VideoObject, to: string[] = []) {
193 const privacy = to.includes(ACTIVITY_PUB.PUBLIC)
194 ? VideoPrivacy.PUBLIC
195 : VideoPrivacy.UNLISTED
196
197 const language = videoObject.language?.identifier
198
199 const category = videoObject.category
200 ? parseInt(videoObject.category.identifier, 10)
201 : undefined
202
203 const licence = videoObject.licence
204 ? parseInt(videoObject.licence.identifier, 10)
205 : undefined
206
207 const description = videoObject.content || null
208 const support = videoObject.support || null
209
210 return {
211 name: videoObject.name,
212 uuid: videoObject.uuid,
213 url: videoObject.id,
214 category,
215 licence,
216 language,
217 description,
218 support,
219 nsfw: videoObject.sensitive,
220 commentsEnabled: videoObject.commentsEnabled,
221 downloadEnabled: videoObject.downloadEnabled,
222 waitTranscoding: videoObject.waitTranscoding,
223 isLive: videoObject.isLiveBroadcast,
224 state: videoObject.state,
225 channelId: videoChannel.id,
226 duration: getDurationFromActivityStream(videoObject.duration),
227 createdAt: new Date(videoObject.published),
228 publishedAt: new Date(videoObject.published),
229
230 originallyPublishedAt: videoObject.originallyPublishedAt
231 ? new Date(videoObject.originallyPublishedAt)
232 : null,
233
234 inputFileUpdatedAt: videoObject.uploadDate
235 ? new Date(videoObject.uploadDate)
236 : null,
237
238 updatedAt: new Date(videoObject.updated),
239 views: videoObject.views,
240 remote: true,
241 privacy
242 }
243}
244
245// ---------------------------------------------------------------------------
246
247export {
248 getThumbnailFromIcons,
249 getPreviewFromIcons,
250
251 getTagsFromObject,
252
253 getFileAttributesFromUrl,
254 getStreamingPlaylistAttributesFromObject,
255
256 getLiveAttributesFromObject,
257 getCaptionAttributesFromObject,
258 getStoryboardAttributeFromObject,
259
260 getVideoAttributesFromObject
261}
262
263// ---------------------------------------------------------------------------
264
265function isAPVideoUrlObject (url: any): url is ActivityVideoUrlObject {
266 const urlMediaType = url.mediaType
267
268 return MIMETYPES.VIDEO.MIMETYPE_EXT[urlMediaType] && urlMediaType.startsWith('video/')
269}
270
271function isAPStreamingPlaylistUrlObject (url: any): url is ActivityPlaylistUrlObject {
272 return url && url.mediaType === 'application/x-mpegURL'
273}
274
275function isAPPlaylistSegmentHashesUrlObject (tag: any): tag is ActivityPlaylistSegmentHashesObject {
276 return tag && tag.name === 'sha256' && tag.type === 'Link' && tag.mediaType === 'application/json'
277}
278
279function isAPMagnetUrlObject (url: any): url is ActivityMagnetUrlObject {
280 return url && url.mediaType === 'application/x-bittorrent;x-scheme-handler/magnet'
281}
282
283function isAPHashTagObject (url: any): url is ActivityHashTagObject {
284 return url && url.type === 'Hashtag'
285}
diff --git a/server/lib/activitypub/videos/shared/trackers.ts b/server/lib/activitypub/videos/shared/trackers.ts
deleted file mode 100644
index 2418f45c2..000000000
--- a/server/lib/activitypub/videos/shared/trackers.ts
+++ /dev/null
@@ -1,43 +0,0 @@
1import { Transaction } from 'sequelize/types'
2import { isAPVideoTrackerUrlObject } from '@server/helpers/custom-validators/activitypub/videos'
3import { isArray } from '@server/helpers/custom-validators/misc'
4import { REMOTE_SCHEME } from '@server/initializers/constants'
5import { TrackerModel } from '@server/models/server/tracker'
6import { MVideo, MVideoWithHost } from '@server/types/models'
7import { ActivityTrackerUrlObject, VideoObject } from '@shared/models'
8import { buildRemoteVideoBaseUrl } from '../../url'
9
10function getTrackerUrls (object: VideoObject, video: MVideoWithHost) {
11 let wsFound = false
12
13 const trackers = object.url.filter(u => isAPVideoTrackerUrlObject(u))
14 .map((u: ActivityTrackerUrlObject) => {
15 if (isArray(u.rel) && u.rel.includes('websocket')) wsFound = true
16
17 return u.href
18 })
19
20 if (wsFound) return trackers
21
22 return [
23 buildRemoteVideoBaseUrl(video, '/tracker/socket', REMOTE_SCHEME.WS),
24 buildRemoteVideoBaseUrl(video, '/tracker/announce')
25 ]
26}
27
28async function setVideoTrackers (options: {
29 video: MVideo
30 trackers: string[]
31 transaction: Transaction
32}) {
33 const { video, trackers, transaction } = options
34
35 const trackerInstances = await TrackerModel.findOrCreateTrackers(trackers, transaction)
36
37 await video.$set('Trackers', trackerInstances, { transaction })
38}
39
40export {
41 getTrackerUrls,
42 setVideoTrackers
43}
diff --git a/server/lib/activitypub/videos/shared/url-to-object.ts b/server/lib/activitypub/videos/shared/url-to-object.ts
deleted file mode 100644
index 7fe008419..000000000
--- a/server/lib/activitypub/videos/shared/url-to-object.ts
+++ /dev/null
@@ -1,25 +0,0 @@
1import { sanitizeAndCheckVideoTorrentObject } from '@server/helpers/custom-validators/activitypub/videos'
2import { logger, loggerTagsFactory } from '@server/helpers/logger'
3import { VideoObject } from '@shared/models'
4import { fetchAP } from '../../activity'
5import { checkUrlsSameHost } from '../../url'
6
7const lTags = loggerTagsFactory('ap', 'video')
8
9async function fetchRemoteVideo (videoUrl: string): Promise<{ statusCode: number, videoObject: VideoObject }> {
10 logger.info('Fetching remote video %s.', videoUrl, lTags(videoUrl))
11
12 const { statusCode, body } = await fetchAP<any>(videoUrl)
13
14 if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) {
15 logger.debug('Remote video JSON is not valid.', { body, ...lTags(videoUrl) })
16
17 return { statusCode, videoObject: undefined }
18 }
19
20 return { statusCode, videoObject: body }
21}
22
23export {
24 fetchRemoteVideo
25}
diff --git a/server/lib/activitypub/videos/shared/video-sync-attributes.ts b/server/lib/activitypub/videos/shared/video-sync-attributes.ts
deleted file mode 100644
index 7fb933559..000000000
--- a/server/lib/activitypub/videos/shared/video-sync-attributes.ts
+++ /dev/null
@@ -1,107 +0,0 @@
1import { runInReadCommittedTransaction } from '@server/helpers/database-utils'
2import { logger, loggerTagsFactory } from '@server/helpers/logger'
3import { JobQueue } from '@server/lib/job-queue'
4import { VideoModel } from '@server/models/video/video'
5import { VideoCommentModel } from '@server/models/video/video-comment'
6import { VideoShareModel } from '@server/models/video/video-share'
7import { MVideo } from '@server/types/models'
8import { ActivitypubHttpFetcherPayload, ActivityPubOrderedCollection, VideoObject } from '@shared/models'
9import { fetchAP } from '../../activity'
10import { crawlCollectionPage } from '../../crawl'
11import { addVideoShares } from '../../share'
12import { addVideoComments } from '../../video-comments'
13
14const lTags = loggerTagsFactory('ap', 'video')
15
16type SyncParam = {
17 rates: boolean
18 shares: boolean
19 comments: boolean
20 refreshVideo?: boolean
21}
22
23async function syncVideoExternalAttributes (
24 video: MVideo,
25 fetchedVideo: VideoObject,
26 syncParam: Pick<SyncParam, 'rates' | 'shares' | 'comments'>
27) {
28 logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid)
29
30 const ratePromise = updateVideoRates(video, fetchedVideo)
31 if (syncParam.rates) await ratePromise
32
33 await syncShares(video, fetchedVideo, syncParam.shares)
34
35 await syncComments(video, fetchedVideo, syncParam.comments)
36}
37
38async function updateVideoRates (video: MVideo, fetchedVideo: VideoObject) {
39 const [ likes, dislikes ] = await Promise.all([
40 getRatesCount('like', video, fetchedVideo),
41 getRatesCount('dislike', video, fetchedVideo)
42 ])
43
44 return runInReadCommittedTransaction(async t => {
45 await VideoModel.updateRatesOf(video.id, 'like', likes, t)
46 await VideoModel.updateRatesOf(video.id, 'dislike', dislikes, t)
47 })
48}
49
50// ---------------------------------------------------------------------------
51
52export {
53 SyncParam,
54 syncVideoExternalAttributes,
55 updateVideoRates
56}
57
58// ---------------------------------------------------------------------------
59
60async function getRatesCount (type: 'like' | 'dislike', video: MVideo, fetchedVideo: VideoObject) {
61 const uri = type === 'like'
62 ? fetchedVideo.likes
63 : fetchedVideo.dislikes
64
65 logger.info('Sync %s of video %s', type, video.url)
66
67 const { body } = await fetchAP<ActivityPubOrderedCollection<any>>(uri)
68
69 if (isNaN(body.totalItems)) {
70 logger.error('Cannot sync %s of video %s, totalItems is not a number', type, video.url, { body })
71 return
72 }
73
74 return body.totalItems
75}
76
77function syncShares (video: MVideo, fetchedVideo: VideoObject, isSync: boolean) {
78 const uri = fetchedVideo.shares
79
80 if (!isSync) {
81 return createJob({ uri, videoId: video.id, type: 'video-shares' })
82 }
83
84 const handler = items => addVideoShares(items, video)
85 const cleaner = crawlStartDate => VideoShareModel.cleanOldSharesOf(video.id, crawlStartDate)
86
87 return crawlCollectionPage<string>(uri, handler, cleaner)
88 .catch(err => logger.error('Cannot add shares of video %s.', video.uuid, { err, rootUrl: uri, ...lTags(video.uuid, video.url) }))
89}
90
91function syncComments (video: MVideo, fetchedVideo: VideoObject, isSync: boolean) {
92 const uri = fetchedVideo.comments
93
94 if (!isSync) {
95 return createJob({ uri, videoId: video.id, type: 'video-comments' })
96 }
97
98 const handler = items => addVideoComments(items)
99 const cleaner = crawlStartDate => VideoCommentModel.cleanOldCommentsOf(video.id, crawlStartDate)
100
101 return crawlCollectionPage<string>(uri, handler, cleaner)
102 .catch(err => logger.error('Cannot add comments of video %s.', video.uuid, { err, rootUrl: uri, ...lTags(video.uuid, video.url) }))
103}
104
105function createJob (payload: ActivitypubHttpFetcherPayload) {
106 return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
107}
diff --git a/server/lib/activitypub/videos/updater.ts b/server/lib/activitypub/videos/updater.ts
deleted file mode 100644
index acb087895..000000000
--- a/server/lib/activitypub/videos/updater.ts
+++ /dev/null
@@ -1,180 +0,0 @@
1import { Transaction } from 'sequelize/types'
2import { resetSequelizeInstance, runInReadCommittedTransaction } from '@server/helpers/database-utils'
3import { logger, loggerTagsFactory, LoggerTagsFn } from '@server/helpers/logger'
4import { Notifier } from '@server/lib/notifier'
5import { PeerTubeSocket } from '@server/lib/peertube-socket'
6import { Hooks } from '@server/lib/plugins/hooks'
7import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist'
8import { VideoLiveModel } from '@server/models/video/video-live'
9import { MActor, MChannelAccountLight, MChannelId, MVideoAccountLightBlacklistAllFiles, MVideoFullLight } from '@server/types/models'
10import { VideoObject, VideoPrivacy } from '@shared/models'
11import { APVideoAbstractBuilder, getVideoAttributesFromObject, updateVideoRates } from './shared'
12
13export class APVideoUpdater extends APVideoAbstractBuilder {
14 private readonly wasPrivateVideo: boolean
15 private readonly wasUnlistedVideo: boolean
16
17 private readonly oldVideoChannel: MChannelAccountLight
18
19 protected lTags: LoggerTagsFn
20
21 constructor (
22 protected readonly videoObject: VideoObject,
23 private readonly video: MVideoAccountLightBlacklistAllFiles
24 ) {
25 super()
26
27 this.wasPrivateVideo = this.video.privacy === VideoPrivacy.PRIVATE
28 this.wasUnlistedVideo = this.video.privacy === VideoPrivacy.UNLISTED
29
30 this.oldVideoChannel = this.video.VideoChannel
31
32 this.lTags = loggerTagsFactory('ap', 'video', 'update', video.uuid, video.url)
33 }
34
35 async update (overrideTo?: string[]) {
36 logger.debug(
37 'Updating remote video "%s".', this.videoObject.uuid,
38 { videoObject: this.videoObject, ...this.lTags() }
39 )
40
41 const oldInputFileUpdatedAt = this.video.inputFileUpdatedAt
42
43 try {
44 const channelActor = await this.getOrCreateVideoChannelFromVideoObject()
45
46 const thumbnailModel = await this.setThumbnail(this.video)
47
48 this.checkChannelUpdateOrThrow(channelActor)
49
50 const videoUpdated = await this.updateVideo(channelActor.VideoChannel, undefined, overrideTo)
51
52 if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel)
53
54 await runInReadCommittedTransaction(async t => {
55 await this.setWebVideoFiles(videoUpdated, t)
56 await this.setStreamingPlaylists(videoUpdated, t)
57 })
58
59 await Promise.all([
60 runInReadCommittedTransaction(t => this.setTags(videoUpdated, t)),
61 runInReadCommittedTransaction(t => this.setTrackers(videoUpdated, t)),
62 runInReadCommittedTransaction(t => this.setStoryboard(videoUpdated, t)),
63 runInReadCommittedTransaction(t => {
64 return Promise.all([
65 this.setPreview(videoUpdated, t),
66 this.setThumbnail(videoUpdated, t)
67 ])
68 }),
69 this.setOrDeleteLive(videoUpdated)
70 ])
71
72 await runInReadCommittedTransaction(t => this.setCaptions(videoUpdated, t))
73
74 await autoBlacklistVideoIfNeeded({
75 video: videoUpdated,
76 user: undefined,
77 isRemote: true,
78 isNew: false,
79 isNewFile: oldInputFileUpdatedAt !== videoUpdated.inputFileUpdatedAt,
80 transaction: undefined
81 })
82
83 await updateVideoRates(videoUpdated, this.videoObject)
84
85 // Notify our users?
86 if (this.wasPrivateVideo || this.wasUnlistedVideo) {
87 Notifier.Instance.notifyOnNewVideoIfNeeded(videoUpdated)
88 }
89
90 if (videoUpdated.isLive) {
91 PeerTubeSocket.Instance.sendVideoLiveNewState(videoUpdated)
92 }
93
94 Hooks.runAction('action:activity-pub.remote-video.updated', { video: videoUpdated, videoAPObject: this.videoObject })
95
96 logger.info('Remote video with uuid %s updated', this.videoObject.uuid, this.lTags())
97
98 return videoUpdated
99 } catch (err) {
100 await this.catchUpdateError(err)
101 }
102 }
103
104 // Check we can update the channel: we trust the remote server
105 private checkChannelUpdateOrThrow (newChannelActor: MActor) {
106 if (!this.oldVideoChannel.Actor.serverId || !newChannelActor.serverId) {
107 throw new Error('Cannot check old channel/new channel validity because `serverId` is null')
108 }
109
110 if (this.oldVideoChannel.Actor.serverId !== newChannelActor.serverId) {
111 throw new Error(`New channel ${newChannelActor.url} is not on the same server than new channel ${this.oldVideoChannel.Actor.url}`)
112 }
113 }
114
115 private updateVideo (channel: MChannelId, transaction?: Transaction, overrideTo?: string[]) {
116 const to = overrideTo || this.videoObject.to
117 const videoData = getVideoAttributesFromObject(channel, this.videoObject, to)
118 this.video.name = videoData.name
119 this.video.uuid = videoData.uuid
120 this.video.url = videoData.url
121 this.video.category = videoData.category
122 this.video.licence = videoData.licence
123 this.video.language = videoData.language
124 this.video.description = videoData.description
125 this.video.support = videoData.support
126 this.video.nsfw = videoData.nsfw
127 this.video.commentsEnabled = videoData.commentsEnabled
128 this.video.downloadEnabled = videoData.downloadEnabled
129 this.video.waitTranscoding = videoData.waitTranscoding
130 this.video.state = videoData.state
131 this.video.duration = videoData.duration
132 this.video.createdAt = videoData.createdAt
133 this.video.publishedAt = videoData.publishedAt
134 this.video.originallyPublishedAt = videoData.originallyPublishedAt
135 this.video.inputFileUpdatedAt = videoData.inputFileUpdatedAt
136 this.video.privacy = videoData.privacy
137 this.video.channelId = videoData.channelId
138 this.video.views = videoData.views
139 this.video.isLive = videoData.isLive
140
141 // Ensures we update the updatedAt attribute, even if main attributes did not change
142 this.video.changed('updatedAt', true)
143
144 return this.video.save({ transaction }) as Promise<MVideoFullLight>
145 }
146
147 private async setCaptions (videoUpdated: MVideoFullLight, t: Transaction) {
148 await this.insertOrReplaceCaptions(videoUpdated, t)
149 }
150
151 private async setStoryboard (videoUpdated: MVideoFullLight, t: Transaction) {
152 await this.insertOrReplaceStoryboard(videoUpdated, t)
153 }
154
155 private async setOrDeleteLive (videoUpdated: MVideoFullLight, transaction?: Transaction) {
156 if (!this.video.isLive) return
157
158 if (this.video.isLive) return this.insertOrReplaceLive(videoUpdated, transaction)
159
160 // Delete existing live if it exists
161 await VideoLiveModel.destroy({
162 where: {
163 videoId: this.video.id
164 },
165 transaction
166 })
167
168 videoUpdated.VideoLive = null
169 }
170
171 private async catchUpdateError (err: Error) {
172 if (this.video !== undefined) {
173 await resetSequelizeInstance(this.video)
174 }
175
176 // This is just a debug because we will retry the insert
177 logger.debug('Cannot update the remote video.', { err, ...this.lTags() })
178 throw err
179 }
180}