diff options
Diffstat (limited to 'server/lib/activitypub/videos/update.ts')
-rw-r--r-- | server/lib/activitypub/videos/update.ts | 293 |
1 files changed, 0 insertions, 293 deletions
diff --git a/server/lib/activitypub/videos/update.ts b/server/lib/activitypub/videos/update.ts deleted file mode 100644 index 444b51628..000000000 --- a/server/lib/activitypub/videos/update.ts +++ /dev/null | |||
@@ -1,293 +0,0 @@ | |||
1 | import { Transaction } from 'sequelize/types' | ||
2 | import { deleteNonExistingModels, resetSequelizeInstance } from '@server/helpers/database-utils' | ||
3 | import { logger } 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 { createPlaceholderThumbnail, createVideoMiniatureFromUrl } from '@server/lib/thumbnail' | ||
8 | import { setVideoTags } from '@server/lib/video' | ||
9 | import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist' | ||
10 | import { VideoCaptionModel } from '@server/models/video/video-caption' | ||
11 | import { VideoFileModel } from '@server/models/video/video-file' | ||
12 | import { VideoLiveModel } from '@server/models/video/video-live' | ||
13 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' | ||
14 | import { | ||
15 | MChannelAccountLight, | ||
16 | MChannelDefault, | ||
17 | MStreamingPlaylistFilesVideo, | ||
18 | MThumbnail, | ||
19 | MVideoAccountLightBlacklistAllFiles, | ||
20 | MVideoCaption, | ||
21 | MVideoFile, | ||
22 | MVideoFullLight | ||
23 | } from '@server/types/models' | ||
24 | import { ThumbnailType, VideoObject, VideoPrivacy } from '@shared/models' | ||
25 | import { | ||
26 | getPreviewFromIcons, | ||
27 | getTagsFromObject, | ||
28 | getThumbnailFromIcons, | ||
29 | getTrackerUrls, | ||
30 | setVideoTrackers, | ||
31 | streamingPlaylistActivityUrlToDBAttributes, | ||
32 | videoActivityObjectToDBAttributes, | ||
33 | videoFileActivityUrlToDBAttributes | ||
34 | } from './shared' | ||
35 | |||
36 | export class APVideoUpdater { | ||
37 | private readonly video: MVideoAccountLightBlacklistAllFiles | ||
38 | private readonly videoObject: VideoObject | ||
39 | private readonly channel: MChannelDefault | ||
40 | private readonly overrideTo: string[] | ||
41 | |||
42 | private readonly wasPrivateVideo: boolean | ||
43 | private readonly wasUnlistedVideo: boolean | ||
44 | |||
45 | private readonly videoFieldsSave: any | ||
46 | |||
47 | private readonly oldVideoChannel: MChannelAccountLight | ||
48 | |||
49 | constructor (options: { | ||
50 | video: MVideoAccountLightBlacklistAllFiles | ||
51 | videoObject: VideoObject | ||
52 | channel: MChannelDefault | ||
53 | overrideTo?: string[] | ||
54 | }) { | ||
55 | this.video = options.video | ||
56 | this.videoObject = options.videoObject | ||
57 | this.channel = options.channel | ||
58 | this.overrideTo = options.overrideTo | ||
59 | |||
60 | this.wasPrivateVideo = this.video.privacy === VideoPrivacy.PRIVATE | ||
61 | this.wasUnlistedVideo = this.video.privacy === VideoPrivacy.UNLISTED | ||
62 | |||
63 | this.oldVideoChannel = this.video.VideoChannel | ||
64 | |||
65 | this.videoFieldsSave = this.video.toJSON() | ||
66 | } | ||
67 | |||
68 | async update () { | ||
69 | logger.debug('Updating remote video "%s".', this.videoObject.uuid, { videoObject: this.videoObject, channel: this.channel }) | ||
70 | |||
71 | try { | ||
72 | const thumbnailModel = await this.tryToGenerateThumbnail() | ||
73 | |||
74 | const videoUpdated = await sequelizeTypescript.transaction(async t => { | ||
75 | this.checkChannelUpdateOrThrow() | ||
76 | |||
77 | const videoUpdated = await this.updateVideo(t) | ||
78 | |||
79 | await this.processIcons(videoUpdated, thumbnailModel, t) | ||
80 | await this.processWebTorrentFiles(videoUpdated, t) | ||
81 | await this.processStreamingPlaylists(videoUpdated, t) | ||
82 | await this.processTags(videoUpdated, t) | ||
83 | await this.processTrackers(videoUpdated, t) | ||
84 | await this.processCaptions(videoUpdated, t) | ||
85 | await this.processLive(videoUpdated, t) | ||
86 | |||
87 | return videoUpdated | ||
88 | }) | ||
89 | |||
90 | await autoBlacklistVideoIfNeeded({ | ||
91 | video: videoUpdated, | ||
92 | user: undefined, | ||
93 | isRemote: true, | ||
94 | isNew: false, | ||
95 | transaction: undefined | ||
96 | }) | ||
97 | |||
98 | // Notify our users? | ||
99 | if (this.wasPrivateVideo || this.wasUnlistedVideo) { | ||
100 | Notifier.Instance.notifyOnNewVideoIfNeeded(videoUpdated) | ||
101 | } | ||
102 | |||
103 | if (videoUpdated.isLive) { | ||
104 | PeerTubeSocket.Instance.sendVideoLiveNewState(videoUpdated) | ||
105 | PeerTubeSocket.Instance.sendVideoViewsUpdate(videoUpdated) | ||
106 | } | ||
107 | |||
108 | logger.info('Remote video with uuid %s updated', this.videoObject.uuid) | ||
109 | |||
110 | return videoUpdated | ||
111 | } catch (err) { | ||
112 | this.catchUpdateError(err) | ||
113 | } | ||
114 | } | ||
115 | |||
116 | private tryToGenerateThumbnail (): Promise<MThumbnail> { | ||
117 | return createVideoMiniatureFromUrl({ | ||
118 | downloadUrl: getThumbnailFromIcons(this.videoObject).url, | ||
119 | video: this.video, | ||
120 | type: ThumbnailType.MINIATURE | ||
121 | }).catch(err => { | ||
122 | logger.warn('Cannot generate thumbnail of %s.', this.videoObject.id, { err }) | ||
123 | |||
124 | return undefined | ||
125 | }) | ||
126 | } | ||
127 | |||
128 | // Check we can update the channel: we trust the remote server | ||
129 | private checkChannelUpdateOrThrow () { | ||
130 | if (!this.oldVideoChannel.Actor.serverId || !this.channel.Actor.serverId) { | ||
131 | throw new Error('Cannot check old channel/new channel validity because `serverId` is null') | ||
132 | } | ||
133 | |||
134 | if (this.oldVideoChannel.Actor.serverId !== this.channel.Actor.serverId) { | ||
135 | throw new Error(`New channel ${this.channel.Actor.url} is not on the same server than new channel ${this.oldVideoChannel.Actor.url}`) | ||
136 | } | ||
137 | } | ||
138 | |||
139 | private updateVideo (transaction: Transaction) { | ||
140 | const to = this.overrideTo || this.videoObject.to | ||
141 | const videoData = videoActivityObjectToDBAttributes(this.channel, this.videoObject, to) | ||
142 | this.video.name = videoData.name | ||
143 | this.video.uuid = videoData.uuid | ||
144 | this.video.url = videoData.url | ||
145 | this.video.category = videoData.category | ||
146 | this.video.licence = videoData.licence | ||
147 | this.video.language = videoData.language | ||
148 | this.video.description = videoData.description | ||
149 | this.video.support = videoData.support | ||
150 | this.video.nsfw = videoData.nsfw | ||
151 | this.video.commentsEnabled = videoData.commentsEnabled | ||
152 | this.video.downloadEnabled = videoData.downloadEnabled | ||
153 | this.video.waitTranscoding = videoData.waitTranscoding | ||
154 | this.video.state = videoData.state | ||
155 | this.video.duration = videoData.duration | ||
156 | this.video.createdAt = videoData.createdAt | ||
157 | this.video.publishedAt = videoData.publishedAt | ||
158 | this.video.originallyPublishedAt = videoData.originallyPublishedAt | ||
159 | this.video.privacy = videoData.privacy | ||
160 | this.video.channelId = videoData.channelId | ||
161 | this.video.views = videoData.views | ||
162 | this.video.isLive = videoData.isLive | ||
163 | |||
164 | // Ensures we update the updated video attribute | ||
165 | this.video.changed('updatedAt', true) | ||
166 | |||
167 | return this.video.save({ transaction }) as Promise<MVideoFullLight> | ||
168 | } | ||
169 | |||
170 | private async processIcons (videoUpdated: MVideoFullLight, thumbnailModel: MThumbnail, t: Transaction) { | ||
171 | if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t) | ||
172 | |||
173 | // Don't fetch the preview that could be big, create a placeholder instead | ||
174 | const previewIcon = getPreviewFromIcons(this.videoObject) | ||
175 | if (videoUpdated.getPreview() && previewIcon) { | ||
176 | const previewModel = createPlaceholderThumbnail({ | ||
177 | fileUrl: previewIcon.url, | ||
178 | video: videoUpdated, | ||
179 | type: ThumbnailType.PREVIEW, | ||
180 | size: previewIcon | ||
181 | }) | ||
182 | await videoUpdated.addAndSaveThumbnail(previewModel, t) | ||
183 | } | ||
184 | } | ||
185 | |||
186 | private async processWebTorrentFiles (videoUpdated: MVideoFullLight, t: Transaction) { | ||
187 | const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoUpdated, this.videoObject.url) | ||
188 | const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a)) | ||
189 | |||
190 | // Remove video files that do not exist anymore | ||
191 | const destroyTasks = deleteNonExistingModels(videoUpdated.VideoFiles, newVideoFiles, t) | ||
192 | await Promise.all(destroyTasks) | ||
193 | |||
194 | // Update or add other one | ||
195 | const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'video', t)) | ||
196 | videoUpdated.VideoFiles = await Promise.all(upsertTasks) | ||
197 | } | ||
198 | |||
199 | private async processStreamingPlaylists (videoUpdated: MVideoFullLight, t: Transaction) { | ||
200 | const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes(videoUpdated, this.videoObject, videoUpdated.VideoFiles) | ||
201 | const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a)) | ||
202 | |||
203 | // Remove video playlists that do not exist anymore | ||
204 | const destroyTasks = deleteNonExistingModels(videoUpdated.VideoStreamingPlaylists, newStreamingPlaylists, t) | ||
205 | await Promise.all(destroyTasks) | ||
206 | |||
207 | let oldStreamingPlaylistFiles: MVideoFile[] = [] | ||
208 | for (const videoStreamingPlaylist of videoUpdated.VideoStreamingPlaylists) { | ||
209 | oldStreamingPlaylistFiles = oldStreamingPlaylistFiles.concat(videoStreamingPlaylist.VideoFiles) | ||
210 | } | ||
211 | |||
212 | videoUpdated.VideoStreamingPlaylists = [] | ||
213 | |||
214 | for (const playlistAttributes of streamingPlaylistAttributes) { | ||
215 | const streamingPlaylistModel = await VideoStreamingPlaylistModel.upsert(playlistAttributes, { returning: true, transaction: t }) | ||
216 | .then(([ streamingPlaylist ]) => streamingPlaylist as MStreamingPlaylistFilesVideo) | ||
217 | streamingPlaylistModel.Video = videoUpdated | ||
218 | |||
219 | const newVideoFiles: MVideoFile[] = videoFileActivityUrlToDBAttributes(streamingPlaylistModel, playlistAttributes.tagAPObject) | ||
220 | .map(a => new VideoFileModel(a)) | ||
221 | const destroyTasks = deleteNonExistingModels(oldStreamingPlaylistFiles, newVideoFiles, t) | ||
222 | await Promise.all(destroyTasks) | ||
223 | |||
224 | // Update or add other one | ||
225 | const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'streaming-playlist', t)) | ||
226 | streamingPlaylistModel.VideoFiles = await Promise.all(upsertTasks) | ||
227 | |||
228 | videoUpdated.VideoStreamingPlaylists.push(streamingPlaylistModel) | ||
229 | } | ||
230 | } | ||
231 | |||
232 | private async processTags (videoUpdated: MVideoFullLight, t: Transaction) { | ||
233 | const tags = getTagsFromObject(this.videoObject) | ||
234 | await setVideoTags({ video: videoUpdated, tags, transaction: t }) | ||
235 | } | ||
236 | |||
237 | private async processTrackers (videoUpdated: MVideoFullLight, t: Transaction) { | ||
238 | const trackers = getTrackerUrls(this.videoObject, videoUpdated) | ||
239 | await setVideoTrackers({ video: videoUpdated, trackers, transaction: t }) | ||
240 | } | ||
241 | |||
242 | private async processCaptions (videoUpdated: MVideoFullLight, t: Transaction) { | ||
243 | // Update captions | ||
244 | await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(videoUpdated.id, t) | ||
245 | |||
246 | const videoCaptionsPromises = this.videoObject.subtitleLanguage.map(c => { | ||
247 | const caption = new VideoCaptionModel({ | ||
248 | videoId: videoUpdated.id, | ||
249 | filename: VideoCaptionModel.generateCaptionName(c.identifier), | ||
250 | language: c.identifier, | ||
251 | fileUrl: c.url | ||
252 | }) as MVideoCaption | ||
253 | |||
254 | return VideoCaptionModel.insertOrReplaceLanguage(caption, t) | ||
255 | }) | ||
256 | |||
257 | await Promise.all(videoCaptionsPromises) | ||
258 | } | ||
259 | |||
260 | private async processLive (videoUpdated: MVideoFullLight, t: Transaction) { | ||
261 | // Create or update existing live | ||
262 | if (this.video.isLive) { | ||
263 | const [ videoLive ] = await VideoLiveModel.upsert({ | ||
264 | saveReplay: this.videoObject.liveSaveReplay, | ||
265 | permanentLive: this.videoObject.permanentLive, | ||
266 | videoId: this.video.id | ||
267 | }, { transaction: t, returning: true }) | ||
268 | |||
269 | videoUpdated.VideoLive = videoLive | ||
270 | return | ||
271 | } | ||
272 | |||
273 | // Delete existing live if it exists | ||
274 | await VideoLiveModel.destroy({ | ||
275 | where: { | ||
276 | videoId: this.video.id | ||
277 | }, | ||
278 | transaction: t | ||
279 | }) | ||
280 | |||
281 | videoUpdated.VideoLive = null | ||
282 | } | ||
283 | |||
284 | private catchUpdateError (err: Error) { | ||
285 | if (this.video !== undefined && this.videoFieldsSave !== undefined) { | ||
286 | resetSequelizeInstance(this.video, this.videoFieldsSave) | ||
287 | } | ||
288 | |||
289 | // This is just a debug because we will retry the insert | ||
290 | logger.debug('Cannot update the remote video.', { err }) | ||
291 | throw err | ||
292 | } | ||
293 | } | ||