]>
Commit | Line | Data |
---|---|---|
69290ab3 C |
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 | } |