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