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