aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/activitypub
diff options
context:
space:
mode:
Diffstat (limited to 'server/lib/activitypub')
-rw-r--r--server/lib/activitypub/videos.ts141
1 files changed, 84 insertions, 57 deletions
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index c318978fd..d80173e03 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -3,8 +3,10 @@ import * as sequelize from 'sequelize'
3import * as magnetUtil from 'magnet-uri' 3import * as magnetUtil from 'magnet-uri'
4import * as request from 'request' 4import * as request from 'request'
5import { 5import {
6 ActivityHashTagObject,
7 ActivityMagnetUrlObject,
6 ActivityPlaylistSegmentHashesObject, 8 ActivityPlaylistSegmentHashesObject,
7 ActivityPlaylistUrlObject, 9 ActivityPlaylistUrlObject, ActivityTagObject,
8 ActivityUrlObject, 10 ActivityUrlObject,
9 ActivityVideoUrlObject, 11 ActivityVideoUrlObject,
10 VideoState 12 VideoState
@@ -13,7 +15,7 @@ import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
13import { VideoPrivacy } from '../../../shared/models/videos' 15import { VideoPrivacy } from '../../../shared/models/videos'
14import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' 16import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos'
15import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' 17import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
16import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' 18import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
17import { logger } from '../../helpers/logger' 19import { logger } from '../../helpers/logger'
18import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' 20import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
19import { 21import {
@@ -57,6 +59,7 @@ import {
57 MChannelAccountLight, 59 MChannelAccountLight,
58 MChannelDefault, 60 MChannelDefault,
59 MChannelId, 61 MChannelId,
62 MStreamingPlaylist,
60 MVideo, 63 MVideo,
61 MVideoAccountLight, 64 MVideoAccountLight,
62 MVideoAccountLightBlacklistAllFiles, 65 MVideoAccountLightBlacklistAllFiles,
@@ -330,21 +333,15 @@ async function updateVideoFromAP (options: {
330 await videoUpdated.addAndSaveThumbnail(previewModel, t) 333 await videoUpdated.addAndSaveThumbnail(previewModel, t)
331 334
332 { 335 {
333 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoUpdated, videoObject) 336 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoUpdated, videoObject.url)
334 const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a)) 337 const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a))
335 338
336 // Remove video files that do not exist anymore 339 // Remove video files that do not exist anymore
337 const destroyTasks = videoUpdated.VideoFiles 340 const destroyTasks = deleteNonExistingModels(videoUpdated.VideoFiles, newVideoFiles, t)
338 .filter(f => !newVideoFiles.find(newFile => newFile.hasSameUniqueKeysThan(f)))
339 .map(f => f.destroy(sequelizeOptions))
340 await Promise.all(destroyTasks) 341 await Promise.all(destroyTasks)
341 342
342 // Update or add other one 343 // Update or add other one
343 const upsertTasks = videoFileAttributes.map(a => { 344 const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'video', t))
344 return VideoFileModel.upsert<VideoFileModel>(a, { returning: true, transaction: t })
345 .then(([ file ]) => file)
346 })
347
348 videoUpdated.VideoFiles = await Promise.all(upsertTasks) 345 videoUpdated.VideoFiles = await Promise.all(upsertTasks)
349 } 346 }
350 347
@@ -352,24 +349,39 @@ async function updateVideoFromAP (options: {
352 const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes(videoUpdated, videoObject, videoUpdated.VideoFiles) 349 const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes(videoUpdated, videoObject, videoUpdated.VideoFiles)
353 const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a)) 350 const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a))
354 351
355 // Remove video files that do not exist anymore 352 // Remove video playlists that do not exist anymore
356 const destroyTasks = videoUpdated.VideoStreamingPlaylists 353 const destroyTasks = deleteNonExistingModels(videoUpdated.VideoStreamingPlaylists, newStreamingPlaylists, t)
357 .filter(f => !newStreamingPlaylists.find(newPlaylist => newPlaylist.hasSameUniqueKeysThan(f)))
358 .map(f => f.destroy(sequelizeOptions))
359 await Promise.all(destroyTasks) 354 await Promise.all(destroyTasks)
360 355
361 // Update or add other one 356 let oldStreamingPlaylistFiles: MVideoFile[] = []
362 const upsertTasks = streamingPlaylistAttributes.map(a => { 357 for (const videoStreamingPlaylist of videoUpdated.VideoStreamingPlaylists) {
363 return VideoStreamingPlaylistModel.upsert<VideoStreamingPlaylistModel>(a, { returning: true, transaction: t }) 358 oldStreamingPlaylistFiles = oldStreamingPlaylistFiles.concat(videoStreamingPlaylist.VideoFiles)
364 .then(([ streamingPlaylist ]) => streamingPlaylist) 359 }
365 }) 360
361 videoUpdated.VideoStreamingPlaylists = []
362
363 for (const playlistAttributes of streamingPlaylistAttributes) {
364 const streamingPlaylistModel = await VideoStreamingPlaylistModel.upsert(playlistAttributes, { returning: true, transaction: t })
365 .then(([ streamingPlaylist ]) => streamingPlaylist)
366 366
367 videoUpdated.VideoStreamingPlaylists = await Promise.all(upsertTasks) 367 const newVideoFiles: MVideoFile[] = videoFileActivityUrlToDBAttributes(streamingPlaylistModel, playlistAttributes.tagAPObject)
368 .map(a => new VideoFileModel(a))
369 const destroyTasks = deleteNonExistingModels(oldStreamingPlaylistFiles, newVideoFiles, t)
370 await Promise.all(destroyTasks)
371
372 // Update or add other one
373 const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'streaming-playlist', t))
374 streamingPlaylistModel.VideoFiles = await Promise.all(upsertTasks)
375
376 videoUpdated.VideoStreamingPlaylists.push(streamingPlaylistModel)
377 }
368 } 378 }
369 379
370 { 380 {
371 // Update Tags 381 // Update Tags
372 const tags = videoObject.tag.map(tag => tag.name) 382 const tags = videoObject.tag
383 .filter(isAPHashTagObject)
384 .map(tag => tag.name)
373 const tagInstances = await TagModel.findOrCreateTags(tags, t) 385 const tagInstances = await TagModel.findOrCreateTags(tags, t)
374 await videoUpdated.$set('Tags', tagInstances, sequelizeOptions) 386 await videoUpdated.$set('Tags', tagInstances, sequelizeOptions)
375 } 387 }
@@ -478,23 +490,27 @@ export {
478 490
479// --------------------------------------------------------------------------- 491// ---------------------------------------------------------------------------
480 492
481function isAPVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject { 493function isAPVideoUrlObject (url: any): url is ActivityVideoUrlObject {
482 const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT) 494 const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT)
483 495
484 const urlMediaType = url.mediaType || url.mimeType 496 const urlMediaType = url.mediaType
485 return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/') 497 return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/')
486} 498}
487 499
488function isAPStreamingPlaylistUrlObject (url: ActivityUrlObject): url is ActivityPlaylistUrlObject { 500function isAPStreamingPlaylistUrlObject (url: ActivityUrlObject): url is ActivityPlaylistUrlObject {
489 const urlMediaType = url.mediaType || url.mimeType 501 return url && url.mediaType === 'application/x-mpegURL'
490
491 return urlMediaType === 'application/x-mpegURL'
492} 502}
493 503
494function isAPPlaylistSegmentHashesUrlObject (tag: any): tag is ActivityPlaylistSegmentHashesObject { 504function isAPPlaylistSegmentHashesUrlObject (tag: any): tag is ActivityPlaylistSegmentHashesObject {
495 const urlMediaType = tag.mediaType || tag.mimeType 505 return tag && tag.name === 'sha256' && tag.type === 'Link' && tag.mediaType === 'application/json'
506}
507
508function isAPMagnetUrlObject (url: any): url is ActivityMagnetUrlObject {
509 return url && url.mediaType === 'application/x-bittorrent;x-scheme-handler/magnet'
510}
496 511
497 return tag.name === 'sha256' && tag.type === 'Link' && urlMediaType === 'application/json' 512function isAPHashTagObject (url: any): url is ActivityHashTagObject {
513 return url && url.type === 'Hashtag'
498} 514}
499 515
500async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAccountLight, waitThumbnail = false) { 516async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAccountLight, waitThumbnail = false) {
@@ -524,21 +540,27 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc
524 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t) 540 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t)
525 541
526 // Process files 542 // Process files
527 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject) 543 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject.url)
528 if (videoFileAttributes.length === 0) {
529 throw new Error('Cannot find valid files for video %s ' + videoObject.url)
530 }
531 544
532 const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t })) 545 const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t }))
533 const videoFiles = await Promise.all(videoFilePromises) 546 const videoFiles = await Promise.all(videoFilePromises)
534 547
535 const videoStreamingPlaylists = streamingPlaylistActivityUrlToDBAttributes(videoCreated, videoObject, videoFiles) 548 const streamingPlaylistsAttributes = streamingPlaylistActivityUrlToDBAttributes(videoCreated, videoObject, videoFiles)
536 const playlistPromises = videoStreamingPlaylists.map(p => VideoStreamingPlaylistModel.create(p, { transaction: t })) 549 videoCreated.VideoStreamingPlaylists = []
537 const streamingPlaylists = await Promise.all(playlistPromises) 550
551 for (const playlistAttributes of streamingPlaylistsAttributes) {
552 const playlistModel = await VideoStreamingPlaylistModel.create(playlistAttributes, { transaction: t })
553
554 const playlistFiles = videoFileActivityUrlToDBAttributes(playlistModel, playlistAttributes.tagAPObject)
555 const videoFilePromises = playlistFiles.map(f => VideoFileModel.create(f, { transaction: t }))
556 playlistModel.VideoFiles = await Promise.all(videoFilePromises)
557
558 videoCreated.VideoStreamingPlaylists.push(playlistModel)
559 }
538 560
539 // Process tags 561 // Process tags
540 const tags = videoObject.tag 562 const tags = videoObject.tag
541 .filter(t => t.type === 'Hashtag') 563 .filter(isAPHashTagObject)
542 .map(t => t.name) 564 .map(t => t.name)
543 const tagInstances = await TagModel.findOrCreateTags(tags, t) 565 const tagInstances = await TagModel.findOrCreateTags(tags, t)
544 await videoCreated.$set('Tags', tagInstances, sequelizeOptions) 566 await videoCreated.$set('Tags', tagInstances, sequelizeOptions)
@@ -550,7 +572,6 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc
550 await Promise.all(videoCaptionsPromises) 572 await Promise.all(videoCaptionsPromises)
551 573
552 videoCreated.VideoFiles = videoFiles 574 videoCreated.VideoFiles = videoFiles
553 videoCreated.VideoStreamingPlaylists = streamingPlaylists
554 videoCreated.Tags = tagInstances 575 videoCreated.Tags = tagInstances
555 576
556 const autoBlacklisted = await autoBlacklistVideoIfNeeded({ 577 const autoBlacklisted = await autoBlacklistVideoIfNeeded({
@@ -628,20 +649,19 @@ async function videoActivityObjectToDBAttributes (videoChannel: MChannelId, vide
628 } 649 }
629} 650}
630 651
631function videoFileActivityUrlToDBAttributes (video: MVideo, videoObject: VideoTorrentObject) { 652function videoFileActivityUrlToDBAttributes (
632 const fileUrls = videoObject.url.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[] 653 videoOrPlaylist: MVideo | MStreamingPlaylist,
654 urls: (ActivityTagObject | ActivityUrlObject)[]
655) {
656 const fileUrls = urls.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
633 657
634 if (fileUrls.length === 0) { 658 if (fileUrls.length === 0) return []
635 throw new Error('Cannot find video files for ' + video.url)
636 }
637 659
638 const attributes: FilteredModelAttributes<VideoFileModel>[] = [] 660 const attributes: FilteredModelAttributes<VideoFileModel>[] = []
639 for (const fileUrl of fileUrls) { 661 for (const fileUrl of fileUrls) {
640 // Fetch associated magnet uri 662 // Fetch associated magnet uri
641 const magnet = videoObject.url.find(u => { 663 const magnet = urls.filter(isAPMagnetUrlObject)
642 const mediaType = u.mediaType || u.mimeType 664 .find(u => u.height === fileUrl.height)
643 return mediaType === 'application/x-bittorrent;x-scheme-handler/magnet' && (u as any).height === fileUrl.height
644 })
645 665
646 if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.href) 666 if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.href)
647 667
@@ -650,14 +670,17 @@ function videoFileActivityUrlToDBAttributes (video: MVideo, videoObject: VideoTo
650 throw new Error('Cannot parse magnet URI ' + magnet.href) 670 throw new Error('Cannot parse magnet URI ' + magnet.href)
651 } 671 }
652 672
653 const mediaType = fileUrl.mediaType || fileUrl.mimeType 673 const mediaType = fileUrl.mediaType
654 const attribute = { 674 const attribute = {
655 extname: MIMETYPES.VIDEO.MIMETYPE_EXT[ mediaType ], 675 extname: MIMETYPES.VIDEO.MIMETYPE_EXT[ mediaType ],
656 infoHash: parsed.infoHash, 676 infoHash: parsed.infoHash,
657 resolution: fileUrl.height, 677 resolution: fileUrl.height,
658 size: fileUrl.size, 678 size: fileUrl.size,
659 videoId: video.id, 679 fps: fileUrl.fps || -1,
660 fps: fileUrl.fps || -1 680
681 // This is a video file owned by a video or by a streaming playlist
682 videoId: (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? null : videoOrPlaylist.id,
683 videoStreamingPlaylistId: (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? videoOrPlaylist.id : null
661 } 684 }
662 685
663 attributes.push(attribute) 686 attributes.push(attribute)
@@ -670,12 +693,15 @@ function streamingPlaylistActivityUrlToDBAttributes (video: MVideoId, videoObjec
670 const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[] 693 const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[]
671 if (playlistUrls.length === 0) return [] 694 if (playlistUrls.length === 0) return []
672 695
673 const attributes: FilteredModelAttributes<VideoStreamingPlaylistModel>[] = [] 696 const attributes: (FilteredModelAttributes<VideoStreamingPlaylistModel> & { tagAPObject?: ActivityTagObject[] })[] = []
674 for (const playlistUrlObject of playlistUrls) { 697 for (const playlistUrlObject of playlistUrls) {
675 const segmentsSha256UrlObject = playlistUrlObject.tag 698 const segmentsSha256UrlObject = playlistUrlObject.tag.find(isAPPlaylistSegmentHashesUrlObject)
676 .find(t => { 699
677 return isAPPlaylistSegmentHashesUrlObject(t) 700 let files: unknown[] = playlistUrlObject.tag.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
678 }) as ActivityPlaylistSegmentHashesObject 701
702 // FIXME: backward compatibility introduced in v2.1.0
703 if (files.length === 0) files = videoFiles
704
679 if (!segmentsSha256UrlObject) { 705 if (!segmentsSha256UrlObject) {
680 logger.warn('No segment sha256 URL found in AP playlist object.', { playlistUrl: playlistUrlObject }) 706 logger.warn('No segment sha256 URL found in AP playlist object.', { playlistUrl: playlistUrlObject })
681 continue 707 continue
@@ -685,9 +711,10 @@ function streamingPlaylistActivityUrlToDBAttributes (video: MVideoId, videoObjec
685 type: VideoStreamingPlaylistType.HLS, 711 type: VideoStreamingPlaylistType.HLS,
686 playlistUrl: playlistUrlObject.href, 712 playlistUrl: playlistUrlObject.href,
687 segmentsSha256Url: segmentsSha256UrlObject.href, 713 segmentsSha256Url: segmentsSha256UrlObject.href,
688 p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrlObject.href, videoFiles), 714 p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrlObject.href, files),
689 p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION, 715 p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
690 videoId: video.id 716 videoId: video.id,
717 tagAPObject: playlistUrlObject.tag
691 } 718 }
692 719
693 attributes.push(attribute) 720 attributes.push(attribute)