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