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