]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/lib/activitypub/videos.ts
Fix video import CLI script
[github/Chocobozzz/PeerTube.git] / server / lib / activitypub / videos.ts
CommitLineData
7acee6f1 1import * as Bluebird from 'bluebird'
2186386c 2import * as sequelize from 'sequelize'
2ccaeeb3 3import * as magnetUtil from 'magnet-uri'
892211e8 4import * as request from 'request'
09209296 5import {
09209296
C
6 ActivityPlaylistSegmentHashesObject,
7 ActivityPlaylistUrlObject,
8 ActivityUrlObject,
1735c825 9 ActivityVideoUrlObject,
09209296
C
10 VideoState
11} from '../../../shared/index'
2ccaeeb3 12import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
1297eb5d 13import { VideoPrivacy } from '../../../shared/models/videos'
1d6e5dfc 14import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos'
2ccaeeb3 15import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
c48e82b5 16import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
2ccaeeb3 17import { logger } from '../../helpers/logger'
dc852737 18import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
e8bafea3
C
19import {
20 ACTIVITY_PUB,
21 MIMETYPES,
22 P2P_MEDIA_LOADER_PEER_VERSION,
23 PREVIEWS_SIZE,
24 REMOTE_SCHEME,
25 STATIC_PATHS
26} from '../../initializers/constants'
2ccaeeb3
C
27import { ActorModel } from '../../models/activitypub/actor'
28import { TagModel } from '../../models/video/tag'
3fd3ab2d 29import { VideoModel } from '../../models/video/video'
2ccaeeb3
C
30import { VideoChannelModel } from '../../models/video/video-channel'
31import { VideoFileModel } from '../../models/video/video-file'
c48e82b5 32import { getOrCreateActorAndServerAndModel } from './actor'
7acee6f1 33import { addVideoComments } from './video-comments'
8fffe21a 34import { crawlCollectionPage } from './crawl'
2186386c 35import { sendCreateVideo, sendUpdateVideo } from './send'
40e87e9e
C
36import { isArray } from '../../helpers/custom-validators/misc'
37import { VideoCaptionModel } from '../../models/video/video-caption'
f6eebcb3
C
38import { JobQueue } from '../job-queue'
39import { ActivitypubHttpFetcherPayload } from '../job-queue/handlers/activitypub-http-fetcher'
1297eb5d
C
40import { createRates } from './video-rates'
41import { addVideoShares, shareVideoByServerAndChannel } from './share'
42import { AccountModel } from '../../models/account/account'
4157cdb1 43import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video'
848f499d 44import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
cef534ed 45import { Notifier } from '../notifier'
09209296
C
46import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
47import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
2ba92871
C
48import { AccountVideoRateModel } from '../../models/account/account-video-rate'
49import { VideoShareModel } from '../../models/video/video-share'
50import { VideoCommentModel } from '../../models/video/video-comment'
74dc3bca 51import { sequelizeTypescript } from '../../initializers/database'
3acc5084 52import { createPlaceholderThumbnail, createVideoMiniatureFromUrl } from '../thumbnail'
e8bafea3
C
53import { ThumbnailModel } from '../../models/video/thumbnail'
54import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
55import { join } from 'path'
3acc5084 56import { FilteredModelAttributes } from '../../typings/sequelize'
2186386c
C
57
58async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
7ccddd7b 59 // If the video is not private and is published, we federate it
2186386c 60 if (video.privacy !== VideoPrivacy.PRIVATE && video.state === VideoState.PUBLISHED) {
40e87e9e
C
61 // Fetch more attributes that we will need to serialize in AP object
62 if (isArray(video.VideoCaptions) === false) {
63 video.VideoCaptions = await video.$get('VideoCaptions', {
64 attributes: [ 'language' ],
65 transaction
66 }) as VideoCaptionModel[]
67 }
68
2cebd797 69 if (isNewVideo) {
2186386c
C
70 // Now we'll add the video's meta data to our followers
71 await sendCreateVideo(video, transaction)
72 await shareVideoByServerAndChannel(video, transaction)
73 } else {
74 await sendUpdateVideo(video, transaction)
75 }
76 }
77}
892211e8 78
4157cdb1
C
79async function fetchRemoteVideo (videoUrl: string): Promise<{ response: request.RequestResponse, videoObject: VideoTorrentObject }> {
80 const options = {
81 uri: videoUrl,
82 method: 'GET',
83 json: true,
84 activityPub: true
85 }
892211e8 86
4157cdb1
C
87 logger.info('Fetching remote video %s.', videoUrl)
88
89 const { response, body } = await doRequest(options)
90
5c6d985f 91 if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) {
4157cdb1
C
92 logger.debug('Remote video JSON is not valid.', { body })
93 return { response, videoObject: undefined }
94 }
95
96 return { response, videoObject: body }
892211e8
C
97}
98
3fd3ab2d 99async function fetchRemoteVideoDescription (video: VideoModel) {
50d6de9c 100 const host = video.VideoChannel.Account.Actor.Server.host
96f29c0f 101 const path = video.getDescriptionAPIPath()
892211e8
C
102 const options = {
103 uri: REMOTE_SCHEME.HTTP + '://' + host + path,
104 json: true
105 }
106
107 const { body } = await doRequest(options)
108 return body.description ? body.description : ''
109}
110
dc852737 111function fetchRemoteVideoStaticFile (video: VideoModel, path: string, destPath: string) {
e8bafea3 112 const url = buildRemoteBaseUrl(video, path)
4157cdb1
C
113
114 // We need to provide a callback, if no we could have an uncaught exception
dc852737 115 return doRequestAndSaveToFile({ uri: url }, destPath)
4157cdb1
C
116}
117
e8bafea3
C
118function buildRemoteBaseUrl (video: VideoModel, path: string) {
119 const host = video.VideoChannel.Account.Actor.Server.host
892211e8 120
e8bafea3 121 return REMOTE_SCHEME.HTTP + '://' + host + path
892211e8
C
122}
123
f37dc0dd 124function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) {
0f320037
C
125 const channel = videoObject.attributedTo.find(a => a.type === 'Group')
126 if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url)
127
5c6d985f
C
128 if (checkUrlsSameHost(channel.id, videoObject.id) !== true) {
129 throw new Error(`Video channel url ${channel.id} does not have the same host than video object id ${videoObject.id}`)
130 }
131
e587e0ec 132 return getOrCreateActorAndServerAndModel(channel.id, 'all')
0f320037
C
133}
134
f6eebcb3 135type SyncParam = {
1297eb5d
C
136 likes: boolean
137 dislikes: boolean
138 shares: boolean
139 comments: boolean
f6eebcb3 140 thumbnail: boolean
04b8c3fb 141 refreshVideo?: boolean
f6eebcb3 142}
4157cdb1 143async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: VideoTorrentObject, syncParam: SyncParam) {
f6eebcb3 144 logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid)
2ccaeeb3 145
f6eebcb3 146 const jobPayloads: ActivitypubHttpFetcherPayload[] = []
2ccaeeb3 147
f6eebcb3 148 if (syncParam.likes === true) {
2ba92871
C
149 const handler = items => createRates(items, video, 'like')
150 const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'like' as 'like', crawlStartDate)
151
152 await crawlCollectionPage<string>(fetchedVideo.likes, handler, cleaner)
f6eebcb3
C
153 .catch(err => logger.error('Cannot add likes of video %s.', video.uuid, { err }))
154 } else {
155 jobPayloads.push({ uri: fetchedVideo.likes, videoId: video.id, type: 'video-likes' as 'video-likes' })
156 }
7acee6f1 157
f6eebcb3 158 if (syncParam.dislikes === true) {
2ba92871
C
159 const handler = items => createRates(items, video, 'dislike')
160 const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'dislike' as 'dislike', crawlStartDate)
161
162 await crawlCollectionPage<string>(fetchedVideo.dislikes, handler, cleaner)
f6eebcb3
C
163 .catch(err => logger.error('Cannot add dislikes of video %s.', video.uuid, { err }))
164 } else {
165 jobPayloads.push({ uri: fetchedVideo.dislikes, videoId: video.id, type: 'video-dislikes' as 'video-dislikes' })
166 }
167
168 if (syncParam.shares === true) {
2ba92871
C
169 const handler = items => addVideoShares(items, video)
170 const cleaner = crawlStartDate => VideoShareModel.cleanOldSharesOf(video.id, crawlStartDate)
171
172 await crawlCollectionPage<string>(fetchedVideo.shares, handler, cleaner)
f6eebcb3
C
173 .catch(err => logger.error('Cannot add shares of video %s.', video.uuid, { err }))
174 } else {
175 jobPayloads.push({ uri: fetchedVideo.shares, videoId: video.id, type: 'video-shares' as 'video-shares' })
176 }
7acee6f1 177
f6eebcb3 178 if (syncParam.comments === true) {
2ba92871
C
179 const handler = items => addVideoComments(items, video)
180 const cleaner = crawlStartDate => VideoCommentModel.cleanOldCommentsOf(video.id, crawlStartDate)
181
182 await crawlCollectionPage<string>(fetchedVideo.comments, handler, cleaner)
f6eebcb3
C
183 .catch(err => logger.error('Cannot add comments of video %s.', video.uuid, { err }))
184 } else {
2ba92871 185 jobPayloads.push({ uri: fetchedVideo.comments, videoId: video.id, type: 'video-comments' as 'video-comments' })
f6eebcb3 186 }
7acee6f1 187
f6eebcb3 188 await Bluebird.map(jobPayloads, payload => JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }))
2ccaeeb3
C
189}
190
4157cdb1 191async function getOrCreateVideoAndAccountAndChannel (options: {
848f499d 192 videoObject: { id: string } | string,
4157cdb1 193 syncParam?: SyncParam,
d4defe07 194 fetchType?: VideoFetchByUrlType,
74577825 195 allowRefresh?: boolean // true by default
4157cdb1
C
196}) {
197 // Default params
198 const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false }
199 const fetchType = options.fetchType || 'all'
74577825 200 const allowRefresh = options.allowRefresh !== false
1297eb5d 201
4157cdb1 202 // Get video url
848f499d 203 const videoUrl = getAPId(options.videoObject)
1297eb5d 204
4157cdb1
C
205 let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType)
206 if (videoFromDatabase) {
f58094b2 207 if (videoFromDatabase.isOutdated() && allowRefresh === true) {
74577825
C
208 const refreshOptions = {
209 video: videoFromDatabase,
210 fetchedType: fetchType,
211 syncParam
212 }
213
214 if (syncParam.refreshVideo === true) videoFromDatabase = await refreshVideoIfNeeded(refreshOptions)
744d0eca 215 else await JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: videoFromDatabase.url } })
d4defe07 216 }
1297eb5d 217
cef534ed 218 return { video: videoFromDatabase, created: false }
1297eb5d
C
219 }
220
4157cdb1
C
221 const { videoObject: fetchedVideo } = await fetchRemoteVideo(videoUrl)
222 if (!fetchedVideo) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
7acee6f1 223
4157cdb1
C
224 const channelActor = await getOrCreateVideoChannelFromVideoObject(fetchedVideo)
225 const video = await retryTransactionWrapper(createVideo, fetchedVideo, channelActor, syncParam.thumbnail)
7acee6f1 226
4157cdb1 227 await syncVideoExternalAttributes(video, fetchedVideo, syncParam)
7acee6f1 228
cef534ed 229 return { video, created: true }
7acee6f1
C
230}
231
d4defe07 232async function updateVideoFromAP (options: {
1297eb5d
C
233 video: VideoModel,
234 videoObject: VideoTorrentObject,
c48e82b5
C
235 account: AccountModel,
236 channel: VideoChannelModel,
1297eb5d 237 overrideTo?: string[]
d4defe07
C
238}) {
239 logger.debug('Updating remote video "%s".', options.videoObject.uuid)
e8d246d5 240
1297eb5d 241 let videoFieldsSave: any
e8d246d5
C
242 const wasPrivateVideo = options.video.privacy === VideoPrivacy.PRIVATE
243 const wasUnlistedVideo = options.video.privacy === VideoPrivacy.UNLISTED
1297eb5d
C
244
245 try {
e8bafea3
C
246 let thumbnailModel: ThumbnailModel
247
248 try {
3acc5084 249 thumbnailModel = await createVideoMiniatureFromUrl(options.videoObject.icon.url, options.video, ThumbnailType.MINIATURE)
e8bafea3
C
250 } catch (err) {
251 logger.warn('Cannot generate thumbnail of %s.', options.videoObject.id, { err })
252 }
253
d382f4e9 254 await sequelizeTypescript.transaction(async t => {
e8d246d5 255 const sequelizeOptions = { transaction: t }
2ccaeeb3 256
d4defe07 257 videoFieldsSave = options.video.toJSON()
2ccaeeb3 258
1297eb5d 259 // Check actor has the right to update the video
d4defe07
C
260 const videoChannel = options.video.VideoChannel
261 if (videoChannel.Account.id !== options.account.id) {
262 throw new Error('Account ' + options.account.Actor.url + ' does not own video channel ' + videoChannel.Actor.url)
f6eebcb3
C
263 }
264
d4defe07
C
265 const to = options.overrideTo ? options.overrideTo : options.videoObject.to
266 const videoData = await videoActivityObjectToDBAttributes(options.channel, options.videoObject, to)
267 options.video.set('name', videoData.name)
268 options.video.set('uuid', videoData.uuid)
269 options.video.set('url', videoData.url)
270 options.video.set('category', videoData.category)
271 options.video.set('licence', videoData.licence)
272 options.video.set('language', videoData.language)
273 options.video.set('description', videoData.description)
274 options.video.set('support', videoData.support)
275 options.video.set('nsfw', videoData.nsfw)
276 options.video.set('commentsEnabled', videoData.commentsEnabled)
7f2cfe3a 277 options.video.set('downloadEnabled', videoData.downloadEnabled)
d4defe07
C
278 options.video.set('waitTranscoding', videoData.waitTranscoding)
279 options.video.set('state', videoData.state)
280 options.video.set('duration', videoData.duration)
281 options.video.set('createdAt', videoData.createdAt)
282 options.video.set('publishedAt', videoData.publishedAt)
7519127b 283 options.video.set('originallyPublishedAt', videoData.originallyPublishedAt)
d4defe07
C
284 options.video.set('privacy', videoData.privacy)
285 options.video.set('channelId', videoData.channelId)
04b8c3fb 286 options.video.set('views', videoData.views)
d4defe07 287
d4defe07 288 await options.video.save(sequelizeOptions)
1297eb5d 289
3acc5084 290 if (thumbnailModel) if (thumbnailModel) await options.video.addAndSaveThumbnail(thumbnailModel, t)
e8bafea3
C
291
292 // FIXME: use icon URL instead
293 const previewUrl = buildRemoteBaseUrl(options.video, join(STATIC_PATHS.PREVIEWS, options.video.getPreview().filename))
294 const previewModel = createPlaceholderThumbnail(previewUrl, options.video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
3acc5084 295 await options.video.addAndSaveThumbnail(previewModel, t)
e8bafea3 296
e5565833
C
297 {
298 const videoFileAttributes = videoFileActivityUrlToDBAttributes(options.video, options.videoObject)
299 const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a))
0032ebe9 300
e5565833
C
301 // Remove video files that do not exist anymore
302 const destroyTasks = options.video.VideoFiles
303 .filter(f => !newVideoFiles.find(newFile => newFile.hasSameUniqueKeysThan(f)))
304 .map(f => f.destroy(sequelizeOptions))
305 await Promise.all(destroyTasks)
2ccaeeb3 306
e5565833 307 // Update or add other one
d382f4e9 308 const upsertTasks = videoFileAttributes.map(a => {
3acc5084 309 return VideoFileModel.upsert<VideoFileModel>(a, { returning: true, transaction: t })
d382f4e9
C
310 .then(([ file ]) => file)
311 })
312
313 options.video.VideoFiles = await Promise.all(upsertTasks)
e5565833 314 }
2ccaeeb3 315
09209296 316 {
ae9bbed4
C
317 const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes(
318 options.video,
319 options.videoObject,
320 options.video.VideoFiles
321 )
09209296
C
322 const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a))
323
324 // Remove video files that do not exist anymore
325 const destroyTasks = options.video.VideoStreamingPlaylists
326 .filter(f => !newStreamingPlaylists.find(newPlaylist => newPlaylist.hasSameUniqueKeysThan(f)))
327 .map(f => f.destroy(sequelizeOptions))
328 await Promise.all(destroyTasks)
329
330 // Update or add other one
331 const upsertTasks = streamingPlaylistAttributes.map(a => {
3acc5084 332 return VideoStreamingPlaylistModel.upsert<VideoStreamingPlaylistModel>(a, { returning: true, transaction: t })
09209296
C
333 .then(([ streamingPlaylist ]) => streamingPlaylist)
334 })
335
336 options.video.VideoStreamingPlaylists = await Promise.all(upsertTasks)
337 }
338
e5565833
C
339 {
340 // Update Tags
341 const tags = options.videoObject.tag.map(tag => tag.name)
342 const tagInstances = await TagModel.findOrCreateTags(tags, t)
343 await options.video.$set('Tags', tagInstances, sequelizeOptions)
344 }
2ccaeeb3 345
e5565833
C
346 {
347 // Update captions
348 await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(options.video.id, t)
349
350 const videoCaptionsPromises = options.videoObject.subtitleLanguage.map(c => {
351 return VideoCaptionModel.insertOrReplaceLanguage(options.video.id, c.identifier, t)
352 })
d382f4e9 353 options.video.VideoCaptions = await Promise.all(videoCaptionsPromises)
e5565833 354 }
1297eb5d
C
355 })
356
e8d246d5
C
357 // Notify our users?
358 if (wasPrivateVideo || wasUnlistedVideo) {
359 Notifier.Instance.notifyOnNewVideo(options.video)
360 }
361
d4defe07 362 logger.info('Remote video with uuid %s updated', options.videoObject.uuid)
1297eb5d 363 } catch (err) {
d4defe07
C
364 if (options.video !== undefined && videoFieldsSave !== undefined) {
365 resetSequelizeInstance(options.video, videoFieldsSave)
1297eb5d
C
366 }
367
368 // This is just a debug because we will retry the insert
369 logger.debug('Cannot update the remote video.', { err })
370 throw err
371 }
892211e8 372}
2186386c 373
04b8c3fb
C
374async function refreshVideoIfNeeded (options: {
375 video: VideoModel,
376 fetchedType: VideoFetchByUrlType,
377 syncParam: SyncParam
378}): Promise<VideoModel> {
379 if (!options.video.isOutdated()) return options.video
380
381 // We need more attributes if the argument video was fetched with not enough joints
382 const video = options.fetchedType === 'all' ? options.video : await VideoModel.loadByUrlAndPopulateAccount(options.video.url)
383
384 try {
385 const { response, videoObject } = await fetchRemoteVideo(video.url)
386 if (response.statusCode === 404) {
387 logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url)
388
389 // Video does not exist anymore
390 await video.destroy()
391 return undefined
392 }
393
394 if (videoObject === undefined) {
395 logger.warn('Cannot refresh remote video %s: invalid body.', video.url)
396
397 await video.setAsRefreshed()
398 return video
399 }
400
401 const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject)
402 const account = await AccountModel.load(channelActor.VideoChannel.accountId)
403
404 const updateOptions = {
405 video,
406 videoObject,
407 account,
408 channel: channelActor.VideoChannel
409 }
410 await retryTransactionWrapper(updateVideoFromAP, updateOptions)
411 await syncVideoExternalAttributes(video, videoObject, options.syncParam)
412
413 return video
414 } catch (err) {
415 logger.warn('Cannot refresh video %s.', options.video.url, { err })
416
417 // Don't refresh in loop
418 await video.setAsRefreshed()
419 return video
420 }
892211e8 421}
2186386c
C
422
423export {
1297eb5d 424 updateVideoFromAP,
04b8c3fb 425 refreshVideoIfNeeded,
2186386c
C
426 federateVideoIfNeeded,
427 fetchRemoteVideo,
1297eb5d 428 getOrCreateVideoAndAccountAndChannel,
40e87e9e 429 fetchRemoteVideoStaticFile,
2186386c 430 fetchRemoteVideoDescription,
4157cdb1 431 getOrCreateVideoChannelFromVideoObject
2186386c 432}
c48e82b5
C
433
434// ---------------------------------------------------------------------------
435
09209296 436function isAPVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject {
14e2014a 437 const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT)
c48e82b5 438
e27ff5da
C
439 const urlMediaType = url.mediaType || url.mimeType
440 return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/')
c48e82b5 441}
4157cdb1 442
09209296
C
443function isAPStreamingPlaylistUrlObject (url: ActivityUrlObject): url is ActivityPlaylistUrlObject {
444 const urlMediaType = url.mediaType || url.mimeType
445
446 return urlMediaType === 'application/x-mpegURL'
447}
448
449function isAPPlaylistSegmentHashesUrlObject (tag: any): tag is ActivityPlaylistSegmentHashesObject {
450 const urlMediaType = tag.mediaType || tag.mimeType
451
452 return tag.name === 'sha256' && tag.type === 'Link' && urlMediaType === 'application/json'
c48e82b5 453}
4157cdb1
C
454
455async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) {
456 logger.debug('Adding remote video %s.', videoObject.id)
457
e8bafea3
C
458 const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoObject, videoObject.to)
459 const video = VideoModel.build(videoData)
460
3acc5084 461 const promiseThumbnail = createVideoMiniatureFromUrl(videoObject.icon.url, video, ThumbnailType.MINIATURE)
e8bafea3
C
462
463 let thumbnailModel: ThumbnailModel
464 if (waitThumbnail === true) {
465 thumbnailModel = await promiseThumbnail
466 }
467
4157cdb1
C
468 const videoCreated: VideoModel = await sequelizeTypescript.transaction(async t => {
469 const sequelizeOptions = { transaction: t }
470
4157cdb1 471 const videoCreated = await video.save(sequelizeOptions)
e8bafea3
C
472 videoCreated.VideoChannel = channelActor.VideoChannel
473
3acc5084 474 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
e8bafea3
C
475
476 // FIXME: use icon URL instead
477 const previewUrl = buildRemoteBaseUrl(videoCreated, join(STATIC_PATHS.PREVIEWS, video.generatePreviewName()))
478 const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
3acc5084 479 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t)
4157cdb1
C
480
481 // Process files
482 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject)
483 if (videoFileAttributes.length === 0) {
484 throw new Error('Cannot find valid files for video %s ' + videoObject.url)
485 }
486
487 const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t }))
ae9bbed4 488 const videoFiles = await Promise.all(videoFilePromises)
4157cdb1 489
ae9bbed4 490 const videoStreamingPlaylists = streamingPlaylistActivityUrlToDBAttributes(videoCreated, videoObject, videoFiles)
09209296
C
491 const playlistPromises = videoStreamingPlaylists.map(p => VideoStreamingPlaylistModel.create(p, { transaction: t }))
492 await Promise.all(playlistPromises)
493
4157cdb1 494 // Process tags
09209296
C
495 const tags = videoObject.tag
496 .filter(t => t.type === 'Hashtag')
497 .map(t => t.name)
4157cdb1
C
498 const tagInstances = await TagModel.findOrCreateTags(tags, t)
499 await videoCreated.$set('Tags', tagInstances, sequelizeOptions)
500
501 // Process captions
502 const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
503 return VideoCaptionModel.insertOrReplaceLanguage(videoCreated.id, c.identifier, t)
504 })
505 await Promise.all(videoCaptionsPromises)
506
507 logger.info('Remote video with uuid %s inserted.', videoObject.uuid)
508
4157cdb1
C
509 return videoCreated
510 })
511
e8bafea3
C
512 if (waitThumbnail === false) {
513 promiseThumbnail.then(thumbnailModel => {
514 thumbnailModel = videoCreated.id
4157cdb1 515
e8bafea3
C
516 return thumbnailModel.save()
517 })
518 }
4157cdb1
C
519
520 return videoCreated
521}
522
4157cdb1
C
523async function videoActivityObjectToDBAttributes (
524 videoChannel: VideoChannelModel,
525 videoObject: VideoTorrentObject,
526 to: string[] = []
527) {
528 const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPrivacy.PUBLIC : VideoPrivacy.UNLISTED
529 const duration = videoObject.duration.replace(/[^\d]+/, '')
530
531 let language: string | undefined
532 if (videoObject.language) {
533 language = videoObject.language.identifier
534 }
535
536 let category: number | undefined
537 if (videoObject.category) {
538 category = parseInt(videoObject.category.identifier, 10)
539 }
540
541 let licence: number | undefined
542 if (videoObject.licence) {
543 licence = parseInt(videoObject.licence.identifier, 10)
544 }
545
546 const description = videoObject.content || null
547 const support = videoObject.support || null
548
549 return {
550 name: videoObject.name,
551 uuid: videoObject.uuid,
552 url: videoObject.id,
553 category,
554 licence,
555 language,
556 description,
557 support,
558 nsfw: videoObject.sensitive,
559 commentsEnabled: videoObject.commentsEnabled,
7f2cfe3a 560 downloadEnabled: videoObject.downloadEnabled,
4157cdb1
C
561 waitTranscoding: videoObject.waitTranscoding,
562 state: videoObject.state,
563 channelId: videoChannel.id,
564 duration: parseInt(duration, 10),
565 createdAt: new Date(videoObject.published),
566 publishedAt: new Date(videoObject.published),
7519127b 567 originallyPublishedAt: videoObject.originallyPublishedAt ? new Date(videoObject.originallyPublishedAt) : null,
4157cdb1
C
568 // FIXME: updatedAt does not seems to be considered by Sequelize
569 updatedAt: new Date(videoObject.updated),
570 views: videoObject.views,
571 likes: 0,
572 dislikes: 0,
573 remote: true,
574 privacy
575 }
576}
577
a3737cbf 578function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) {
09209296 579 const fileUrls = videoObject.url.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
4157cdb1
C
580
581 if (fileUrls.length === 0) {
a3737cbf 582 throw new Error('Cannot find video files for ' + video.url)
4157cdb1
C
583 }
584
3acc5084 585 const attributes: FilteredModelAttributes<VideoFileModel>[] = []
4157cdb1
C
586 for (const fileUrl of fileUrls) {
587 // Fetch associated magnet uri
588 const magnet = videoObject.url.find(u => {
e27ff5da
C
589 const mediaType = u.mediaType || u.mimeType
590 return mediaType === 'application/x-bittorrent;x-scheme-handler/magnet' && (u as any).height === fileUrl.height
4157cdb1
C
591 })
592
593 if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.href)
594
595 const parsed = magnetUtil.decode(magnet.href)
596 if (!parsed || isVideoFileInfoHashValid(parsed.infoHash) === false) {
597 throw new Error('Cannot parse magnet URI ' + magnet.href)
598 }
599
e27ff5da 600 const mediaType = fileUrl.mediaType || fileUrl.mimeType
4157cdb1 601 const attribute = {
14e2014a 602 extname: MIMETYPES.VIDEO.MIMETYPE_EXT[ mediaType ],
4157cdb1
C
603 infoHash: parsed.infoHash,
604 resolution: fileUrl.height,
605 size: fileUrl.size,
a3737cbf 606 videoId: video.id,
2e7cf5ae 607 fps: fileUrl.fps || -1
09209296
C
608 }
609
610 attributes.push(attribute)
611 }
612
613 return attributes
614}
615
ae9bbed4 616function streamingPlaylistActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject, videoFiles: VideoFileModel[]) {
09209296
C
617 const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[]
618 if (playlistUrls.length === 0) return []
619
3acc5084 620 const attributes: FilteredModelAttributes<VideoStreamingPlaylistModel>[] = []
09209296 621 for (const playlistUrlObject of playlistUrls) {
09209296
C
622 const segmentsSha256UrlObject = playlistUrlObject.tag
623 .find(t => {
624 return isAPPlaylistSegmentHashesUrlObject(t)
625 }) as ActivityPlaylistSegmentHashesObject
626 if (!segmentsSha256UrlObject) {
627 logger.warn('No segment sha256 URL found in AP playlist object.', { playlistUrl: playlistUrlObject })
628 continue
629 }
630
631 const attribute = {
632 type: VideoStreamingPlaylistType.HLS,
633 playlistUrl: playlistUrlObject.href,
634 segmentsSha256Url: segmentsSha256UrlObject.href,
ae9bbed4 635 p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrlObject.href, videoFiles),
594d0c6a 636 p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
09209296
C
637 videoId: video.id
638 }
639
4157cdb1
C
640 attributes.push(attribute)
641 }
642
643 return attributes
644}