]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/lib/activitypub/videos.ts
Use a single file instead of segments for HLS
[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,
74577825
C
170 fetchType?: VideoFetchByUrlType,
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) {
04b8c3fb 183
74577825
C
184 if (allowRefresh === true) {
185 const refreshOptions = {
186 video: videoFromDatabase,
187 fetchedType: fetchType,
188 syncParam
189 }
190
191 if (syncParam.refreshVideo === true) videoFromDatabase = await refreshVideoIfNeeded(refreshOptions)
744d0eca 192 else await JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: videoFromDatabase.url } })
74577825 193 }
1297eb5d 194
cef534ed 195 return { video: videoFromDatabase, created: false }
1297eb5d
C
196 }
197
4157cdb1
C
198 const { videoObject: fetchedVideo } = await fetchRemoteVideo(videoUrl)
199 if (!fetchedVideo) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
7acee6f1 200
4157cdb1
C
201 const channelActor = await getOrCreateVideoChannelFromVideoObject(fetchedVideo)
202 const video = await retryTransactionWrapper(createVideo, fetchedVideo, channelActor, syncParam.thumbnail)
7acee6f1 203
4157cdb1 204 await syncVideoExternalAttributes(video, fetchedVideo, syncParam)
7acee6f1 205
cef534ed 206 return { video, created: true }
7acee6f1
C
207}
208
d4defe07 209async function updateVideoFromAP (options: {
1297eb5d
C
210 video: VideoModel,
211 videoObject: VideoTorrentObject,
c48e82b5
C
212 account: AccountModel,
213 channel: VideoChannelModel,
1297eb5d 214 overrideTo?: string[]
d4defe07
C
215}) {
216 logger.debug('Updating remote video "%s".', options.videoObject.uuid)
e8d246d5 217
1297eb5d 218 let videoFieldsSave: any
e8d246d5
C
219 const wasPrivateVideo = options.video.privacy === VideoPrivacy.PRIVATE
220 const wasUnlistedVideo = options.video.privacy === VideoPrivacy.UNLISTED
1297eb5d
C
221
222 try {
d382f4e9 223 await sequelizeTypescript.transaction(async t => {
e8d246d5 224 const sequelizeOptions = { transaction: t }
2ccaeeb3 225
d4defe07 226 videoFieldsSave = options.video.toJSON()
2ccaeeb3 227
1297eb5d 228 // Check actor has the right to update the video
d4defe07
C
229 const videoChannel = options.video.VideoChannel
230 if (videoChannel.Account.id !== options.account.id) {
231 throw new Error('Account ' + options.account.Actor.url + ' does not own video channel ' + videoChannel.Actor.url)
f6eebcb3
C
232 }
233
d4defe07
C
234 const to = options.overrideTo ? options.overrideTo : options.videoObject.to
235 const videoData = await videoActivityObjectToDBAttributes(options.channel, options.videoObject, to)
236 options.video.set('name', videoData.name)
237 options.video.set('uuid', videoData.uuid)
238 options.video.set('url', videoData.url)
239 options.video.set('category', videoData.category)
240 options.video.set('licence', videoData.licence)
241 options.video.set('language', videoData.language)
242 options.video.set('description', videoData.description)
243 options.video.set('support', videoData.support)
244 options.video.set('nsfw', videoData.nsfw)
245 options.video.set('commentsEnabled', videoData.commentsEnabled)
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)
251 options.video.set('privacy', videoData.privacy)
252 options.video.set('channelId', videoData.channelId)
04b8c3fb 253 options.video.set('views', videoData.views)
d4defe07 254
d4defe07 255 await options.video.save(sequelizeOptions)
1297eb5d 256
e5565833
C
257 {
258 const videoFileAttributes = videoFileActivityUrlToDBAttributes(options.video, options.videoObject)
259 const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a))
0032ebe9 260
e5565833
C
261 // Remove video files that do not exist anymore
262 const destroyTasks = options.video.VideoFiles
263 .filter(f => !newVideoFiles.find(newFile => newFile.hasSameUniqueKeysThan(f)))
264 .map(f => f.destroy(sequelizeOptions))
265 await Promise.all(destroyTasks)
2ccaeeb3 266
e5565833 267 // Update or add other one
d382f4e9
C
268 const upsertTasks = videoFileAttributes.map(a => {
269 return VideoFileModel.upsert<VideoFileModel>(a, { returning: true, transaction: t })
270 .then(([ file ]) => file)
271 })
272
273 options.video.VideoFiles = await Promise.all(upsertTasks)
e5565833 274 }
2ccaeeb3 275
09209296
C
276 {
277 const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes(options.video, options.videoObject)
278 const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a))
279
280 // Remove video files that do not exist anymore
281 const destroyTasks = options.video.VideoStreamingPlaylists
282 .filter(f => !newStreamingPlaylists.find(newPlaylist => newPlaylist.hasSameUniqueKeysThan(f)))
283 .map(f => f.destroy(sequelizeOptions))
284 await Promise.all(destroyTasks)
285
286 // Update or add other one
287 const upsertTasks = streamingPlaylistAttributes.map(a => {
288 return VideoStreamingPlaylistModel.upsert<VideoStreamingPlaylistModel>(a, { returning: true, transaction: t })
289 .then(([ streamingPlaylist ]) => streamingPlaylist)
290 })
291
292 options.video.VideoStreamingPlaylists = await Promise.all(upsertTasks)
293 }
294
e5565833
C
295 {
296 // Update Tags
297 const tags = options.videoObject.tag.map(tag => tag.name)
298 const tagInstances = await TagModel.findOrCreateTags(tags, t)
299 await options.video.$set('Tags', tagInstances, sequelizeOptions)
300 }
2ccaeeb3 301
e5565833
C
302 {
303 // Update captions
304 await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(options.video.id, t)
305
306 const videoCaptionsPromises = options.videoObject.subtitleLanguage.map(c => {
307 return VideoCaptionModel.insertOrReplaceLanguage(options.video.id, c.identifier, t)
308 })
d382f4e9 309 options.video.VideoCaptions = await Promise.all(videoCaptionsPromises)
e5565833 310 }
1297eb5d
C
311 })
312
e8d246d5
C
313 // Notify our users?
314 if (wasPrivateVideo || wasUnlistedVideo) {
315 Notifier.Instance.notifyOnNewVideo(options.video)
316 }
317
d4defe07 318 logger.info('Remote video with uuid %s updated', options.videoObject.uuid)
1297eb5d 319 } catch (err) {
d4defe07
C
320 if (options.video !== undefined && videoFieldsSave !== undefined) {
321 resetSequelizeInstance(options.video, videoFieldsSave)
1297eb5d
C
322 }
323
324 // This is just a debug because we will retry the insert
325 logger.debug('Cannot update the remote video.', { err })
326 throw err
327 }
a8a63227
C
328
329 try {
330 await generateThumbnailFromUrl(options.video, options.videoObject.icon)
331 } catch (err) {
332 logger.warn('Cannot generate thumbnail of %s.', options.videoObject.id, { err })
333 }
892211e8 334}
2186386c 335
04b8c3fb
C
336async function refreshVideoIfNeeded (options: {
337 video: VideoModel,
338 fetchedType: VideoFetchByUrlType,
339 syncParam: SyncParam
340}): Promise<VideoModel> {
341 if (!options.video.isOutdated()) return options.video
342
343 // We need more attributes if the argument video was fetched with not enough joints
344 const video = options.fetchedType === 'all' ? options.video : await VideoModel.loadByUrlAndPopulateAccount(options.video.url)
345
346 try {
347 const { response, videoObject } = await fetchRemoteVideo(video.url)
348 if (response.statusCode === 404) {
349 logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url)
350
351 // Video does not exist anymore
352 await video.destroy()
353 return undefined
354 }
355
356 if (videoObject === undefined) {
357 logger.warn('Cannot refresh remote video %s: invalid body.', video.url)
358
359 await video.setAsRefreshed()
360 return video
361 }
362
363 const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject)
364 const account = await AccountModel.load(channelActor.VideoChannel.accountId)
365
366 const updateOptions = {
367 video,
368 videoObject,
369 account,
370 channel: channelActor.VideoChannel
371 }
372 await retryTransactionWrapper(updateVideoFromAP, updateOptions)
373 await syncVideoExternalAttributes(video, videoObject, options.syncParam)
374
375 return video
376 } catch (err) {
377 logger.warn('Cannot refresh video %s.', options.video.url, { err })
378
379 // Don't refresh in loop
380 await video.setAsRefreshed()
381 return video
382 }
383}
384
2186386c 385export {
1297eb5d 386 updateVideoFromAP,
04b8c3fb 387 refreshVideoIfNeeded,
2186386c
C
388 federateVideoIfNeeded,
389 fetchRemoteVideo,
1297eb5d 390 getOrCreateVideoAndAccountAndChannel,
40e87e9e 391 fetchRemoteVideoStaticFile,
2186386c
C
392 fetchRemoteVideoDescription,
393 generateThumbnailFromUrl,
4157cdb1 394 getOrCreateVideoChannelFromVideoObject
2186386c 395}
c48e82b5
C
396
397// ---------------------------------------------------------------------------
398
09209296 399function isAPVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject {
14e2014a 400 const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT)
c48e82b5 401
e27ff5da
C
402 const urlMediaType = url.mediaType || url.mimeType
403 return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/')
c48e82b5 404}
4157cdb1 405
09209296
C
406function isAPStreamingPlaylistUrlObject (url: ActivityUrlObject): url is ActivityPlaylistUrlObject {
407 const urlMediaType = url.mediaType || url.mimeType
408
409 return urlMediaType === 'application/x-mpegURL'
410}
411
412function isAPPlaylistSegmentHashesUrlObject (tag: any): tag is ActivityPlaylistSegmentHashesObject {
413 const urlMediaType = tag.mediaType || tag.mimeType
414
415 return tag.name === 'sha256' && tag.type === 'Link' && urlMediaType === 'application/json'
416}
417
4157cdb1
C
418async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) {
419 logger.debug('Adding remote video %s.', videoObject.id)
420
421 const videoCreated: VideoModel = await sequelizeTypescript.transaction(async t => {
422 const sequelizeOptions = { transaction: t }
423
424 const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoObject, videoObject.to)
425 const video = VideoModel.build(videoData)
426
427 const videoCreated = await video.save(sequelizeOptions)
428
429 // Process files
430 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject)
431 if (videoFileAttributes.length === 0) {
432 throw new Error('Cannot find valid files for video %s ' + videoObject.url)
433 }
434
435 const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t }))
436 await Promise.all(videoFilePromises)
437
09209296
C
438 const videoStreamingPlaylists = streamingPlaylistActivityUrlToDBAttributes(videoCreated, videoObject)
439 const playlistPromises = videoStreamingPlaylists.map(p => VideoStreamingPlaylistModel.create(p, { transaction: t }))
440 await Promise.all(playlistPromises)
441
4157cdb1 442 // Process tags
09209296
C
443 const tags = videoObject.tag
444 .filter(t => t.type === 'Hashtag')
445 .map(t => t.name)
4157cdb1
C
446 const tagInstances = await TagModel.findOrCreateTags(tags, t)
447 await videoCreated.$set('Tags', tagInstances, sequelizeOptions)
448
449 // Process captions
450 const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
451 return VideoCaptionModel.insertOrReplaceLanguage(videoCreated.id, c.identifier, t)
452 })
453 await Promise.all(videoCaptionsPromises)
454
455 logger.info('Remote video with uuid %s inserted.', videoObject.uuid)
456
457 videoCreated.VideoChannel = channelActor.VideoChannel
458 return videoCreated
459 })
460
461 const p = generateThumbnailFromUrl(videoCreated, videoObject.icon)
462 .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err }))
463
464 if (waitThumbnail === true) await p
465
466 return videoCreated
467}
468
4157cdb1
C
469async function videoActivityObjectToDBAttributes (
470 videoChannel: VideoChannelModel,
471 videoObject: VideoTorrentObject,
472 to: string[] = []
473) {
474 const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPrivacy.PUBLIC : VideoPrivacy.UNLISTED
475 const duration = videoObject.duration.replace(/[^\d]+/, '')
476
477 let language: string | undefined
478 if (videoObject.language) {
479 language = videoObject.language.identifier
480 }
481
482 let category: number | undefined
483 if (videoObject.category) {
484 category = parseInt(videoObject.category.identifier, 10)
485 }
486
487 let licence: number | undefined
488 if (videoObject.licence) {
489 licence = parseInt(videoObject.licence.identifier, 10)
490 }
491
492 const description = videoObject.content || null
493 const support = videoObject.support || null
494
495 return {
496 name: videoObject.name,
497 uuid: videoObject.uuid,
498 url: videoObject.id,
499 category,
500 licence,
501 language,
502 description,
503 support,
504 nsfw: videoObject.sensitive,
505 commentsEnabled: videoObject.commentsEnabled,
506 waitTranscoding: videoObject.waitTranscoding,
507 state: videoObject.state,
508 channelId: videoChannel.id,
509 duration: parseInt(duration, 10),
510 createdAt: new Date(videoObject.published),
511 publishedAt: new Date(videoObject.published),
512 // FIXME: updatedAt does not seems to be considered by Sequelize
513 updatedAt: new Date(videoObject.updated),
514 views: videoObject.views,
515 likes: 0,
516 dislikes: 0,
517 remote: true,
518 privacy
519 }
520}
521
a3737cbf 522function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) {
09209296 523 const fileUrls = videoObject.url.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
4157cdb1
C
524
525 if (fileUrls.length === 0) {
a3737cbf 526 throw new Error('Cannot find video files for ' + video.url)
4157cdb1
C
527 }
528
09209296 529 const attributes: FilteredModelAttributes<VideoFileModel>[] = []
4157cdb1
C
530 for (const fileUrl of fileUrls) {
531 // Fetch associated magnet uri
532 const magnet = videoObject.url.find(u => {
e27ff5da
C
533 const mediaType = u.mediaType || u.mimeType
534 return mediaType === 'application/x-bittorrent;x-scheme-handler/magnet' && (u as any).height === fileUrl.height
4157cdb1
C
535 })
536
537 if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.href)
538
539 const parsed = magnetUtil.decode(magnet.href)
540 if (!parsed || isVideoFileInfoHashValid(parsed.infoHash) === false) {
541 throw new Error('Cannot parse magnet URI ' + magnet.href)
542 }
543
e27ff5da 544 const mediaType = fileUrl.mediaType || fileUrl.mimeType
4157cdb1 545 const attribute = {
14e2014a 546 extname: MIMETYPES.VIDEO.MIMETYPE_EXT[ mediaType ],
4157cdb1
C
547 infoHash: parsed.infoHash,
548 resolution: fileUrl.height,
549 size: fileUrl.size,
a3737cbf 550 videoId: video.id,
2e7cf5ae 551 fps: fileUrl.fps || -1
09209296
C
552 }
553
554 attributes.push(attribute)
555 }
556
557 return attributes
558}
559
560function streamingPlaylistActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) {
561 const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[]
562 if (playlistUrls.length === 0) return []
563
564 const attributes: FilteredModelAttributes<VideoStreamingPlaylistModel>[] = []
565 for (const playlistUrlObject of playlistUrls) {
566 const p2pMediaLoaderInfohashes = playlistUrlObject.tag
567 .filter(t => t.type === 'Infohash')
568 .map(t => t.name)
569 if (p2pMediaLoaderInfohashes.length === 0) {
570 logger.warn('No infohashes found in AP playlist object.', { playlistUrl: playlistUrlObject })
571 continue
572 }
573
574 const segmentsSha256UrlObject = playlistUrlObject.tag
575 .find(t => {
576 return isAPPlaylistSegmentHashesUrlObject(t)
577 }) as ActivityPlaylistSegmentHashesObject
578 if (!segmentsSha256UrlObject) {
579 logger.warn('No segment sha256 URL found in AP playlist object.', { playlistUrl: playlistUrlObject })
580 continue
581 }
582
583 const attribute = {
584 type: VideoStreamingPlaylistType.HLS,
585 playlistUrl: playlistUrlObject.href,
586 segmentsSha256Url: segmentsSha256UrlObject.href,
587 p2pMediaLoaderInfohashes,
588 videoId: video.id
589 }
590
4157cdb1
C
591 attributes.push(attribute)
592 }
593
594 return attributes
595}