diff options
Diffstat (limited to 'server/lib/activitypub')
-rw-r--r-- | server/lib/activitypub/videos.ts | 141 |
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' | |||
3 | import * as magnetUtil from 'magnet-uri' | 3 | import * as magnetUtil from 'magnet-uri' |
4 | import * as request from 'request' | 4 | import * as request from 'request' |
5 | import { | 5 | import { |
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' | |||
13 | import { VideoPrivacy } from '../../../shared/models/videos' | 15 | import { VideoPrivacy } from '../../../shared/models/videos' |
14 | import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' | 16 | import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' |
15 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' | 17 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' |
16 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' | 18 | import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' |
17 | import { logger } from '../../helpers/logger' | 19 | import { logger } from '../../helpers/logger' |
18 | import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' | 20 | import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' |
19 | import { | 21 | import { |
@@ -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 | ||
481 | function isAPVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject { | 493 | function 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 | ||
488 | function isAPStreamingPlaylistUrlObject (url: ActivityUrlObject): url is ActivityPlaylistUrlObject { | 500 | function 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 | ||
494 | function isAPPlaylistSegmentHashesUrlObject (tag: any): tag is ActivityPlaylistSegmentHashesObject { | 504 | function 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 | |||
508 | function 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' | 512 | function isAPHashTagObject (url: any): url is ActivityHashTagObject { |
513 | return url && url.type === 'Hashtag' | ||
498 | } | 514 | } |
499 | 515 | ||
500 | async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAccountLight, waitThumbnail = false) { | 516 | async 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 | ||
631 | function videoFileActivityUrlToDBAttributes (video: MVideo, videoObject: VideoTorrentObject) { | 652 | function 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) |