aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/activitypub/videos.ts
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-02-11 11:52:34 +0100
committerChocobozzz <me@florianbigard.com>2019-02-11 11:52:34 +0100
commit88108880bbdba473cfe36ecbebc1c3c4f972e102 (patch)
treeb242efb3b4f0d7e49d88f2d1f2063b5b3b0489c0 /server/lib/activitypub/videos.ts
parent53a94c7cfa8368da4cd248d65df8346905938f0c (diff)
parent9b712a2017e4ab3cf12cd6bd58278905520159d0 (diff)
downloadPeerTube-88108880bbdba473cfe36ecbebc1c3c4f972e102.tar.gz
PeerTube-88108880bbdba473cfe36ecbebc1c3c4f972e102.tar.zst
PeerTube-88108880bbdba473cfe36ecbebc1c3c4f972e102.zip
Merge branch 'develop' into pr/1217
Diffstat (limited to 'server/lib/activitypub/videos.ts')
-rw-r--r--server/lib/activitypub/videos.ts280
1 files changed, 191 insertions, 89 deletions
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index 8521572a1..710929aac 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -1,17 +1,23 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import * as sequelize from 'sequelize' 2import * as sequelize from 'sequelize'
3import * as magnetUtil from 'magnet-uri' 3import * as magnetUtil from 'magnet-uri'
4import { join } from 'path'
5import * as request from 'request' 4import * as request from 'request'
6import { ActivityIconObject, ActivityUrlObject, ActivityVideoUrlObject, VideoState } from '../../../shared/index' 5import {
6 ActivityIconObject,
7 ActivityPlaylistSegmentHashesObject,
8 ActivityPlaylistUrlObject,
9 ActivityUrlObject,
10 ActivityVideoUrlObject,
11 VideoState
12} from '../../../shared/index'
7import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' 13import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
8import { VideoPrivacy } from '../../../shared/models/videos' 14import { VideoPrivacy } from '../../../shared/models/videos'
9import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' 15import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos'
10import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' 16import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
11import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' 17import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
12import { logger } from '../../helpers/logger' 18import { logger } from '../../helpers/logger'
13import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' 19import { doRequest, downloadImage } from '../../helpers/requests'
14import { ACTIVITY_PUB, CONFIG, REMOTE_SCHEME, sequelizeTypescript, VIDEO_MIMETYPE_EXT } from '../../initializers' 20import { ACTIVITY_PUB, CONFIG, MIMETYPES, REMOTE_SCHEME, sequelizeTypescript, THUMBNAILS_SIZE } from '../../initializers'
15import { ActorModel } from '../../models/activitypub/actor' 21import { ActorModel } from '../../models/activitypub/actor'
16import { TagModel } from '../../models/video/tag' 22import { TagModel } from '../../models/video/tag'
17import { VideoModel } from '../../models/video/video' 23import { VideoModel } from '../../models/video/video'
@@ -29,6 +35,11 @@ import { createRates } from './video-rates'
29import { addVideoShares, shareVideoByServerAndChannel } from './share' 35import { addVideoShares, shareVideoByServerAndChannel } from './share'
30import { AccountModel } from '../../models/account/account' 36import { AccountModel } from '../../models/account/account'
31import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' 37import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video'
38import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
39import { Notifier } from '../notifier'
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'
32 43
33async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { 44async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
34 // If the video is not private and published, we federate it 45 // If the video is not private and published, we federate it
@@ -63,7 +74,7 @@ async function fetchRemoteVideo (videoUrl: string): Promise<{ response: request.
63 74
64 const { response, body } = await doRequest(options) 75 const { response, body } = await doRequest(options)
65 76
66 if (sanitizeAndCheckVideoTorrentObject(body) === false) { 77 if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) {
67 logger.debug('Remote video JSON is not valid.', { body }) 78 logger.debug('Remote video JSON is not valid.', { body })
68 return { response, videoObject: undefined } 79 return { response, videoObject: undefined }
69 } 80 }
@@ -94,19 +105,18 @@ function fetchRemoteVideoStaticFile (video: VideoModel, path: string, reject: Fu
94 105
95function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) { 106function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) {
96 const thumbnailName = video.getThumbnailName() 107 const thumbnailName = video.getThumbnailName()
97 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName)
98 108
99 const options = { 109 return downloadImage(icon.url, CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName, THUMBNAILS_SIZE)
100 method: 'GET',
101 uri: icon.url
102 }
103 return doRequestAndSaveToFile(options, thumbnailPath)
104} 110}
105 111
106function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { 112function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) {
107 const channel = videoObject.attributedTo.find(a => a.type === 'Group') 113 const channel = videoObject.attributedTo.find(a => a.type === 'Group')
108 if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url) 114 if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url)
109 115
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
110 return getOrCreateActorAndServerAndModel(channel.id, 'all') 120 return getOrCreateActorAndServerAndModel(channel.id, 'all')
111} 121}
112 122
@@ -116,7 +126,7 @@ type SyncParam = {
116 shares: boolean 126 shares: boolean
117 comments: boolean 127 comments: boolean
118 thumbnail: boolean 128 thumbnail: boolean
119 refreshVideo: boolean 129 refreshVideo?: boolean
120} 130}
121async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: VideoTorrentObject, syncParam: SyncParam) { 131async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: VideoTorrentObject, syncParam: SyncParam) {
122 logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid) 132 logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid)
@@ -155,31 +165,34 @@ async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: Vid
155} 165}
156 166
157async function getOrCreateVideoAndAccountAndChannel (options: { 167async function getOrCreateVideoAndAccountAndChannel (options: {
158 videoObject: VideoTorrentObject | string, 168 videoObject: { id: string } | string,
159 syncParam?: SyncParam, 169 syncParam?: SyncParam,
160 fetchType?: VideoFetchByUrlType, 170 fetchType?: VideoFetchByUrlType,
161 refreshViews?: boolean 171 allowRefresh?: boolean // true by default
162}) { 172}) {
163 // Default params 173 // Default params
164 const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false } 174 const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false }
165 const fetchType = options.fetchType || 'all' 175 const fetchType = options.fetchType || 'all'
166 const refreshViews = options.refreshViews || false 176 const allowRefresh = options.allowRefresh !== false
167 177
168 // Get video url 178 // Get video url
169 const videoUrl = typeof options.videoObject === 'string' ? options.videoObject : options.videoObject.id 179 const videoUrl = getAPId(options.videoObject)
170 180
171 let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType) 181 let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType)
172 if (videoFromDatabase) { 182 if (videoFromDatabase) {
173 const refreshOptions = { 183
174 video: videoFromDatabase, 184 if (allowRefresh === true) {
175 fetchedType: fetchType, 185 const refreshOptions = {
176 syncParam, 186 video: videoFromDatabase,
177 refreshViews 187 fetchedType: fetchType,
188 syncParam
189 }
190
191 if (syncParam.refreshVideo === true) videoFromDatabase = await refreshVideoIfNeeded(refreshOptions)
192 else await JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: videoFromDatabase.url } })
178 } 193 }
179 const p = refreshVideoIfNeeded(refreshOptions)
180 if (syncParam.refreshVideo === true) videoFromDatabase = await p
181 194
182 return { video: videoFromDatabase } 195 return { video: videoFromDatabase, created: false }
183 } 196 }
184 197
185 const { videoObject: fetchedVideo } = await fetchRemoteVideo(videoUrl) 198 const { videoObject: fetchedVideo } = await fetchRemoteVideo(videoUrl)
@@ -190,7 +203,7 @@ async function getOrCreateVideoAndAccountAndChannel (options: {
190 203
191 await syncVideoExternalAttributes(video, fetchedVideo, syncParam) 204 await syncVideoExternalAttributes(video, fetchedVideo, syncParam)
192 205
193 return { video } 206 return { video, created: true }
194} 207}
195 208
196async function updateVideoFromAP (options: { 209async function updateVideoFromAP (options: {
@@ -198,17 +211,17 @@ async function updateVideoFromAP (options: {
198 videoObject: VideoTorrentObject, 211 videoObject: VideoTorrentObject,
199 account: AccountModel, 212 account: AccountModel,
200 channel: VideoChannelModel, 213 channel: VideoChannelModel,
201 updateViews: boolean,
202 overrideTo?: string[] 214 overrideTo?: string[]
203}) { 215}) {
204 logger.debug('Updating remote video "%s".', options.videoObject.uuid) 216 logger.debug('Updating remote video "%s".', options.videoObject.uuid)
217
205 let videoFieldsSave: any 218 let videoFieldsSave: any
219 const wasPrivateVideo = options.video.privacy === VideoPrivacy.PRIVATE
220 const wasUnlistedVideo = options.video.privacy === VideoPrivacy.UNLISTED
206 221
207 try { 222 try {
208 await sequelizeTypescript.transaction(async t => { 223 await sequelizeTypescript.transaction(async t => {
209 const sequelizeOptions = { 224 const sequelizeOptions = { transaction: t }
210 transaction: t
211 }
212 225
213 videoFieldsSave = options.video.toJSON() 226 videoFieldsSave = options.video.toJSON()
214 227
@@ -238,14 +251,10 @@ async function updateVideoFromAP (options: {
238 options.video.set('publishedAt', videoData.publishedAt) 251 options.video.set('publishedAt', videoData.publishedAt)
239 options.video.set('privacy', videoData.privacy) 252 options.video.set('privacy', videoData.privacy)
240 options.video.set('channelId', videoData.channelId) 253 options.video.set('channelId', videoData.channelId)
254 options.video.set('views', videoData.views)
241 255
242 if (options.updateViews === true) options.video.set('views', videoData.views)
243 await options.video.save(sequelizeOptions) 256 await options.video.save(sequelizeOptions)
244 257
245 // Don't block on request
246 generateThumbnailFromUrl(options.video, options.videoObject.icon)
247 .catch(err => logger.warn('Cannot generate thumbnail of %s.', options.videoObject.id, { err }))
248
249 { 258 {
250 const videoFileAttributes = videoFileActivityUrlToDBAttributes(options.video, options.videoObject) 259 const videoFileAttributes = videoFileActivityUrlToDBAttributes(options.video, options.videoObject)
251 const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a)) 260 const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a))
@@ -266,6 +275,25 @@ async function updateVideoFromAP (options: {
266 } 275 }
267 276
268 { 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
296 {
269 // Update Tags 297 // Update Tags
270 const tags = options.videoObject.tag.map(tag => tag.name) 298 const tags = options.videoObject.tag.map(tag => tag.name)
271 const tagInstances = await TagModel.findOrCreateTags(tags, t) 299 const tagInstances = await TagModel.findOrCreateTags(tags, t)
@@ -283,6 +311,11 @@ async function updateVideoFromAP (options: {
283 } 311 }
284 }) 312 })
285 313
314 // Notify our users?
315 if (wasPrivateVideo || wasUnlistedVideo) {
316 Notifier.Instance.notifyOnNewVideo(options.video)
317 }
318
286 logger.info('Remote video with uuid %s updated', options.videoObject.uuid) 319 logger.info('Remote video with uuid %s updated', options.videoObject.uuid)
287 } catch (err) { 320 } catch (err) {
288 if (options.video !== undefined && videoFieldsSave !== undefined) { 321 if (options.video !== undefined && videoFieldsSave !== undefined) {
@@ -293,10 +326,66 @@ async function updateVideoFromAP (options: {
293 logger.debug('Cannot update the remote video.', { err }) 326 logger.debug('Cannot update the remote video.', { err })
294 throw err 327 throw err
295 } 328 }
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 }
335}
336
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 }
296} 384}
297 385
298export { 386export {
299 updateVideoFromAP, 387 updateVideoFromAP,
388 refreshVideoIfNeeded,
300 federateVideoIfNeeded, 389 federateVideoIfNeeded,
301 fetchRemoteVideo, 390 fetchRemoteVideo,
302 getOrCreateVideoAndAccountAndChannel, 391 getOrCreateVideoAndAccountAndChannel,
@@ -308,10 +397,23 @@ export {
308 397
309// --------------------------------------------------------------------------- 398// ---------------------------------------------------------------------------
310 399
311function isActivityVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject { 400function isAPVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject {
312 const mimeTypes = Object.keys(VIDEO_MIMETYPE_EXT) 401 const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT)
313 402
314 return mimeTypes.indexOf(url.mimeType) !== -1 && url.mimeType.startsWith('video/') 403 const urlMediaType = url.mediaType || url.mimeType
404 return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/')
405}
406
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'
315} 417}
316 418
317async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) { 419async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) {
@@ -334,8 +436,14 @@ async function createVideo (videoObject: VideoTorrentObject, channelActor: Actor
334 const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t })) 436 const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t }))
335 await Promise.all(videoFilePromises) 437 await Promise.all(videoFilePromises)
336 438
439 const videoStreamingPlaylists = streamingPlaylistActivityUrlToDBAttributes(videoCreated, videoObject)
440 const playlistPromises = videoStreamingPlaylists.map(p => VideoStreamingPlaylistModel.create(p, { transaction: t }))
441 await Promise.all(playlistPromises)
442
337 // Process tags 443 // Process tags
338 const tags = videoObject.tag.map(t => t.name) 444 const tags = videoObject.tag
445 .filter(t => t.type === 'Hashtag')
446 .map(t => t.name)
339 const tagInstances = await TagModel.findOrCreateTags(tags, t) 447 const tagInstances = await TagModel.findOrCreateTags(tags, t)
340 await videoCreated.$set('Tags', tagInstances, sequelizeOptions) 448 await videoCreated.$set('Tags', tagInstances, sequelizeOptions)
341 449
@@ -359,52 +467,6 @@ async function createVideo (videoObject: VideoTorrentObject, channelActor: Actor
359 return videoCreated 467 return videoCreated
360} 468}
361 469
362async function refreshVideoIfNeeded (options: {
363 video: VideoModel,
364 fetchedType: VideoFetchByUrlType,
365 syncParam: SyncParam,
366 refreshViews: boolean
367}): Promise<VideoModel> {
368 if (!options.video.isOutdated()) return options.video
369
370 // We need more attributes if the argument video was fetched with not enough joints
371 const video = options.fetchedType === 'all' ? options.video : await VideoModel.loadByUrlAndPopulateAccount(options.video.url)
372
373 try {
374 const { response, videoObject } = await fetchRemoteVideo(video.url)
375 if (response.statusCode === 404) {
376 logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url)
377
378 // Video does not exist anymore
379 await video.destroy()
380 return undefined
381 }
382
383 if (videoObject === undefined) {
384 logger.warn('Cannot refresh remote video %s: invalid body.', video.url)
385 return video
386 }
387
388 const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject)
389 const account = await AccountModel.load(channelActor.VideoChannel.accountId)
390
391 const updateOptions = {
392 video,
393 videoObject,
394 account,
395 channel: channelActor.VideoChannel,
396 updateViews: options.refreshViews
397 }
398 await retryTransactionWrapper(updateVideoFromAP, updateOptions)
399 await syncVideoExternalAttributes(video, videoObject, options.syncParam)
400
401 return video
402 } catch (err) {
403 logger.warn('Cannot refresh video %s.', options.video.url, { err })
404 return video
405 }
406}
407
408async function videoActivityObjectToDBAttributes ( 470async function videoActivityObjectToDBAttributes (
409 videoChannel: VideoChannelModel, 471 videoChannel: VideoChannelModel,
410 videoObject: VideoTorrentObject, 472 videoObject: VideoTorrentObject,
@@ -460,17 +522,18 @@ async function videoActivityObjectToDBAttributes (
460} 522}
461 523
462function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) { 524function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) {
463 const fileUrls = videoObject.url.filter(u => isActivityVideoUrlObject(u)) as ActivityVideoUrlObject[] 525 const fileUrls = videoObject.url.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
464 526
465 if (fileUrls.length === 0) { 527 if (fileUrls.length === 0) {
466 throw new Error('Cannot find video files for ' + video.url) 528 throw new Error('Cannot find video files for ' + video.url)
467 } 529 }
468 530
469 const attributes: VideoFileModel[] = [] 531 const attributes: FilteredModelAttributes<VideoFileModel>[] = []
470 for (const fileUrl of fileUrls) { 532 for (const fileUrl of fileUrls) {
471 // Fetch associated magnet uri 533 // Fetch associated magnet uri
472 const magnet = videoObject.url.find(u => { 534 const magnet = videoObject.url.find(u => {
473 return u.mimeType === 'application/x-bittorrent;x-scheme-handler/magnet' && u.height === fileUrl.height 535 const mediaType = u.mediaType || u.mimeType
536 return mediaType === 'application/x-bittorrent;x-scheme-handler/magnet' && (u as any).height === fileUrl.height
474 }) 537 })
475 538
476 if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.href) 539 if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.href)
@@ -480,14 +543,53 @@ function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: Vid
480 throw new Error('Cannot parse magnet URI ' + magnet.href) 543 throw new Error('Cannot parse magnet URI ' + magnet.href)
481 } 544 }
482 545
546 const mediaType = fileUrl.mediaType || fileUrl.mimeType
483 const attribute = { 547 const attribute = {
484 extname: VIDEO_MIMETYPE_EXT[ fileUrl.mimeType ], 548 extname: MIMETYPES.VIDEO.MIMETYPE_EXT[ mediaType ],
485 infoHash: parsed.infoHash, 549 infoHash: parsed.infoHash,
486 resolution: fileUrl.height, 550 resolution: fileUrl.height,
487 size: fileUrl.size, 551 size: fileUrl.size,
488 videoId: video.id, 552 videoId: video.id,
489 fps: fileUrl.fps || -1 553 fps: fileUrl.fps || -1
490 } as VideoFileModel 554 }
555
556 attributes.push(attribute)
557 }
558
559 return attributes
560}
561
562function streamingPlaylistActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) {
563 const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[]
564 if (playlistUrls.length === 0) return []
565
566 const attributes: FilteredModelAttributes<VideoStreamingPlaylistModel>[] = []
567 for (const playlistUrlObject of playlistUrls) {
568 const p2pMediaLoaderInfohashes = playlistUrlObject.tag
569 .filter(t => t.type === 'Infohash')
570 .map(t => t.name)
571 if (p2pMediaLoaderInfohashes.length === 0) {
572 logger.warn('No infohashes found in AP playlist object.', { playlistUrl: playlistUrlObject })
573 continue
574 }
575
576 const segmentsSha256UrlObject = playlistUrlObject.tag
577 .find(t => {
578 return isAPPlaylistSegmentHashesUrlObject(t)
579 }) as ActivityPlaylistSegmentHashesObject
580 if (!segmentsSha256UrlObject) {
581 logger.warn('No segment sha256 URL found in AP playlist object.', { playlistUrl: playlistUrlObject })
582 continue
583 }
584
585 const attribute = {
586 type: VideoStreamingPlaylistType.HLS,
587 playlistUrl: playlistUrlObject.href,
588 segmentsSha256Url: segmentsSha256UrlObject.href,
589 p2pMediaLoaderInfohashes,
590 videoId: video.id
591 }
592
491 attributes.push(attribute) 593 attributes.push(attribute)
492 } 594 }
493 595