aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/activitypub/videos.ts
diff options
context:
space:
mode:
Diffstat (limited to 'server/lib/activitypub/videos.ts')
-rw-r--r--server/lib/activitypub/videos.ts107
1 files changed, 97 insertions, 10 deletions
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index 893768769..710929aac 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -2,7 +2,14 @@ import * 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 * as request from 'request' 4import * as request from 'request'
5import { ActivityIconObject, ActivityUrlObject, ActivityVideoUrlObject, VideoState } from '../../../shared/index' 5import {
6 ActivityIconObject,
7 ActivityPlaylistSegmentHashesObject,
8 ActivityPlaylistUrlObject,
9 ActivityUrlObject,
10 ActivityVideoUrlObject,
11 VideoState
12} from '../../../shared/index'
6import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' 13import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
7import { VideoPrivacy } from '../../../shared/models/videos' 14import { VideoPrivacy } from '../../../shared/models/videos'
8import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' 15import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos'
@@ -28,8 +35,11 @@ import { createRates } from './video-rates'
28import { addVideoShares, shareVideoByServerAndChannel } from './share' 35import { addVideoShares, shareVideoByServerAndChannel } from './share'
29import { AccountModel } from '../../models/account/account' 36import { AccountModel } from '../../models/account/account'
30import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' 37import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video'
31import { checkUrlsSameHost, getAPUrl } from '../../helpers/activitypub' 38import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
32import { Notifier } from '../notifier' 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'
33 43
34async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { 44async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
35 // If the video is not private and published, we federate it 45 // If the video is not private and published, we federate it
@@ -155,7 +165,7 @@ 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 allowRefresh?: boolean // true by default 171 allowRefresh?: boolean // true by default
@@ -166,7 +176,7 @@ async function getOrCreateVideoAndAccountAndChannel (options: {
166 const allowRefresh = options.allowRefresh !== false 176 const allowRefresh = options.allowRefresh !== false
167 177
168 // Get video url 178 // Get video url
169 const videoUrl = getAPUrl(options.videoObject) 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) {
@@ -179,7 +189,7 @@ async function getOrCreateVideoAndAccountAndChannel (options: {
179 } 189 }
180 190
181 if (syncParam.refreshVideo === true) videoFromDatabase = await refreshVideoIfNeeded(refreshOptions) 191 if (syncParam.refreshVideo === true) videoFromDatabase = await refreshVideoIfNeeded(refreshOptions)
182 else await JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', videoUrl: videoFromDatabase.url } }) 192 else await JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: videoFromDatabase.url } })
183 } 193 }
184 194
185 return { video: videoFromDatabase, created: false } 195 return { video: videoFromDatabase, created: false }
@@ -233,6 +243,7 @@ async function updateVideoFromAP (options: {
233 options.video.set('support', videoData.support) 243 options.video.set('support', videoData.support)
234 options.video.set('nsfw', videoData.nsfw) 244 options.video.set('nsfw', videoData.nsfw)
235 options.video.set('commentsEnabled', videoData.commentsEnabled) 245 options.video.set('commentsEnabled', videoData.commentsEnabled)
246 options.video.set('downloadEnabled', videoData.downloadEnabled)
236 options.video.set('waitTranscoding', videoData.waitTranscoding) 247 options.video.set('waitTranscoding', videoData.waitTranscoding)
237 options.video.set('state', videoData.state) 248 options.video.set('state', videoData.state)
238 options.video.set('duration', videoData.duration) 249 options.video.set('duration', videoData.duration)
@@ -264,6 +275,25 @@ async function updateVideoFromAP (options: {
264 } 275 }
265 276
266 { 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 {
267 // Update Tags 297 // Update Tags
268 const tags = options.videoObject.tag.map(tag => tag.name) 298 const tags = options.videoObject.tag.map(tag => tag.name)
269 const tagInstances = await TagModel.findOrCreateTags(tags, t) 299 const tagInstances = await TagModel.findOrCreateTags(tags, t)
@@ -367,13 +397,25 @@ export {
367 397
368// --------------------------------------------------------------------------- 398// ---------------------------------------------------------------------------
369 399
370function isActivityVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject { 400function isAPVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject {
371 const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT) 401 const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT)
372 402
373 const urlMediaType = url.mediaType || url.mimeType 403 const urlMediaType = url.mediaType || url.mimeType
374 return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/') 404 return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/')
375} 405}
376 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'
417}
418
377async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) { 419async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) {
378 logger.debug('Adding remote video %s.', videoObject.id) 420 logger.debug('Adding remote video %s.', videoObject.id)
379 421
@@ -394,8 +436,14 @@ async function createVideo (videoObject: VideoTorrentObject, channelActor: Actor
394 const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t })) 436 const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t }))
395 await Promise.all(videoFilePromises) 437 await Promise.all(videoFilePromises)
396 438
439 const videoStreamingPlaylists = streamingPlaylistActivityUrlToDBAttributes(videoCreated, videoObject)
440 const playlistPromises = videoStreamingPlaylists.map(p => VideoStreamingPlaylistModel.create(p, { transaction: t }))
441 await Promise.all(playlistPromises)
442
397 // Process tags 443 // Process tags
398 const tags = videoObject.tag.map(t => t.name) 444 const tags = videoObject.tag
445 .filter(t => t.type === 'Hashtag')
446 .map(t => t.name)
399 const tagInstances = await TagModel.findOrCreateTags(tags, t) 447 const tagInstances = await TagModel.findOrCreateTags(tags, t)
400 await videoCreated.$set('Tags', tagInstances, sequelizeOptions) 448 await videoCreated.$set('Tags', tagInstances, sequelizeOptions)
401 449
@@ -456,6 +504,7 @@ async function videoActivityObjectToDBAttributes (
456 support, 504 support,
457 nsfw: videoObject.sensitive, 505 nsfw: videoObject.sensitive,
458 commentsEnabled: videoObject.commentsEnabled, 506 commentsEnabled: videoObject.commentsEnabled,
507 downloadEnabled: videoObject.downloadEnabled,
459 waitTranscoding: videoObject.waitTranscoding, 508 waitTranscoding: videoObject.waitTranscoding,
460 state: videoObject.state, 509 state: videoObject.state,
461 channelId: videoChannel.id, 510 channelId: videoChannel.id,
@@ -473,13 +522,13 @@ async function videoActivityObjectToDBAttributes (
473} 522}
474 523
475function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) { 524function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) {
476 const fileUrls = videoObject.url.filter(u => isActivityVideoUrlObject(u)) as ActivityVideoUrlObject[] 525 const fileUrls = videoObject.url.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
477 526
478 if (fileUrls.length === 0) { 527 if (fileUrls.length === 0) {
479 throw new Error('Cannot find video files for ' + video.url) 528 throw new Error('Cannot find video files for ' + video.url)
480 } 529 }
481 530
482 const attributes: VideoFileModel[] = [] 531 const attributes: FilteredModelAttributes<VideoFileModel>[] = []
483 for (const fileUrl of fileUrls) { 532 for (const fileUrl of fileUrls) {
484 // Fetch associated magnet uri 533 // Fetch associated magnet uri
485 const magnet = videoObject.url.find(u => { 534 const magnet = videoObject.url.find(u => {
@@ -502,7 +551,45 @@ function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: Vid
502 size: fileUrl.size, 551 size: fileUrl.size,
503 videoId: video.id, 552 videoId: video.id,
504 fps: fileUrl.fps || -1 553 fps: fileUrl.fps || -1
505 } 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
506 attributes.push(attribute) 593 attributes.push(attribute)
507 } 594 }
508 595