aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2018-06-12 20:04:58 +0200
committerChocobozzz <me@florianbigard.com>2018-06-12 20:37:51 +0200
commit2186386cca113506791583cb07d6ccacba7af4e0 (patch)
tree3c214c0b5fbd64332624267fa6e51fd4a9cf6474 /server/lib
parent6ccdf3a23ecec5ba2eeaf487fd1fafdc7606b4bf (diff)
downloadPeerTube-2186386cca113506791583cb07d6ccacba7af4e0.tar.gz
PeerTube-2186386cca113506791583cb07d6ccacba7af4e0.tar.zst
PeerTube-2186386cca113506791583cb07d6ccacba7af4e0.zip
Add concept of video state, and add ability to wait transcoding before
publishing a video
Diffstat (limited to 'server/lib')
-rw-r--r--server/lib/activitypub/audience.ts10
-rw-r--r--server/lib/activitypub/crawl.ts2
-rw-r--r--server/lib/activitypub/process/process-update.ts27
-rw-r--r--server/lib/activitypub/send/send-announce.ts14
-rw-r--r--server/lib/activitypub/send/send-create.ts43
-rw-r--r--server/lib/activitypub/send/send-like.ts33
-rw-r--r--server/lib/activitypub/send/send-undo.ts42
-rw-r--r--server/lib/activitypub/send/send-update.ts36
-rw-r--r--server/lib/activitypub/videos.ts80
-rw-r--r--server/lib/job-queue/handlers/video-file.ts127
-rw-r--r--server/lib/job-queue/job-queue.ts1
11 files changed, 215 insertions, 200 deletions
diff --git a/server/lib/activitypub/audience.ts b/server/lib/activitypub/audience.ts
index c1265dbcd..7164135b6 100644
--- a/server/lib/activitypub/audience.ts
+++ b/server/lib/activitypub/audience.ts
@@ -20,7 +20,7 @@ function getVideoCommentAudience (
20 isOrigin = false 20 isOrigin = false
21) { 21) {
22 const to = [ ACTIVITY_PUB.PUBLIC ] 22 const to = [ ACTIVITY_PUB.PUBLIC ]
23 const cc = [ ] 23 const cc = []
24 24
25 // Owner of the video we comment 25 // Owner of the video we comment
26 if (isOrigin === false) { 26 if (isOrigin === false) {
@@ -55,7 +55,7 @@ async function getActorsInvolvedInVideo (video: VideoModel, t: Transaction) {
55 return actors 55 return actors
56} 56}
57 57
58async function getAudience (actorSender: ActorModel, t: Transaction, isPublic = true) { 58function getAudience (actorSender: ActorModel, isPublic = true) {
59 return buildAudience([ actorSender.followersUrl ], isPublic) 59 return buildAudience([ actorSender.followersUrl ], isPublic)
60} 60}
61 61
@@ -67,14 +67,14 @@ function buildAudience (followerUrls: string[], isPublic = true) {
67 to = [ ACTIVITY_PUB.PUBLIC ] 67 to = [ ACTIVITY_PUB.PUBLIC ]
68 cc = followerUrls 68 cc = followerUrls
69 } else { // Unlisted 69 } else { // Unlisted
70 to = [ ] 70 to = []
71 cc = [ ] 71 cc = []
72 } 72 }
73 73
74 return { to, cc } 74 return { to, cc }
75} 75}
76 76
77function audiencify <T> (object: T, audience: ActivityAudience) { 77function audiencify<T> (object: T, audience: ActivityAudience) {
78 return Object.assign(object, audience) 78 return Object.assign(object, audience)
79} 79}
80 80
diff --git a/server/lib/activitypub/crawl.ts b/server/lib/activitypub/crawl.ts
index 7305b3969..d4fc786f7 100644
--- a/server/lib/activitypub/crawl.ts
+++ b/server/lib/activitypub/crawl.ts
@@ -28,7 +28,7 @@ async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Pr
28 28
29 if (Array.isArray(body.orderedItems)) { 29 if (Array.isArray(body.orderedItems)) {
30 const items = body.orderedItems 30 const items = body.orderedItems
31 logger.info('Processing %i ActivityPub items for %s.', items.length, nextLink) 31 logger.info('Processing %i ActivityPub items for %s.', items.length, options.uri)
32 32
33 await handler(items) 33 await handler(items)
34 } 34 }
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts
index 2750f48c3..77de8c155 100644
--- a/server/lib/activitypub/process/process-update.ts
+++ b/server/lib/activitypub/process/process-update.ts
@@ -1,7 +1,6 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { ActivityUpdate } from '../../../../shared/models/activitypub' 2import { ActivityUpdate } from '../../../../shared/models/activitypub'
3import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor' 3import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor'
4import { VideoTorrentObject } from '../../../../shared/models/activitypub/objects'
5import { retryTransactionWrapper } from '../../../helpers/database-utils' 4import { retryTransactionWrapper } from '../../../helpers/database-utils'
6import { logger } from '../../../helpers/logger' 5import { logger } from '../../../helpers/logger'
7import { resetSequelizeInstance } from '../../../helpers/utils' 6import { resetSequelizeInstance } from '../../../helpers/utils'
@@ -13,6 +12,7 @@ import { VideoChannelModel } from '../../../models/video/video-channel'
13import { VideoFileModel } from '../../../models/video/video-file' 12import { VideoFileModel } from '../../../models/video/video-file'
14import { fetchAvatarIfExists, getOrCreateActorAndServerAndModel, updateActorAvatarInstance, updateActorInstance } from '../actor' 13import { fetchAvatarIfExists, getOrCreateActorAndServerAndModel, updateActorAvatarInstance, updateActorInstance } from '../actor'
15import { 14import {
15 fetchRemoteVideo,
16 generateThumbnailFromUrl, 16 generateThumbnailFromUrl,
17 getOrCreateAccountAndVideoAndChannel, 17 getOrCreateAccountAndVideoAndChannel,
18 getOrCreateVideoChannel, 18 getOrCreateVideoChannel,
@@ -51,15 +51,18 @@ function processUpdateVideo (actor: ActorModel, activity: ActivityUpdate) {
51} 51}
52 52
53async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) { 53async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
54 const videoAttributesToUpdate = activity.object as VideoTorrentObject 54 const videoUrl = activity.object.id
55 55
56 const res = await getOrCreateAccountAndVideoAndChannel(videoAttributesToUpdate.id) 56 const videoObject = await fetchRemoteVideo(videoUrl)
57 if (!videoObject) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
58
59 const res = await getOrCreateAccountAndVideoAndChannel(videoObject.id)
57 60
58 // Fetch video channel outside the transaction 61 // Fetch video channel outside the transaction
59 const newVideoChannelActor = await getOrCreateVideoChannel(videoAttributesToUpdate) 62 const newVideoChannelActor = await getOrCreateVideoChannel(videoObject)
60 const newVideoChannel = newVideoChannelActor.VideoChannel 63 const newVideoChannel = newVideoChannelActor.VideoChannel
61 64
62 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid) 65 logger.debug('Updating remote video "%s".', videoObject.uuid)
63 let videoInstance = res.video 66 let videoInstance = res.video
64 let videoFieldsSave: any 67 let videoFieldsSave: any
65 68
@@ -77,7 +80,7 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
77 throw new Error('Account ' + actor.url + ' does not own video channel ' + videoChannel.Actor.url) 80 throw new Error('Account ' + actor.url + ' does not own video channel ' + videoChannel.Actor.url)
78 } 81 }
79 82
80 const videoData = await videoActivityObjectToDBAttributes(newVideoChannel, videoAttributesToUpdate, activity.to) 83 const videoData = await videoActivityObjectToDBAttributes(newVideoChannel, videoObject, activity.to)
81 videoInstance.set('name', videoData.name) 84 videoInstance.set('name', videoData.name)
82 videoInstance.set('uuid', videoData.uuid) 85 videoInstance.set('uuid', videoData.uuid)
83 videoInstance.set('url', videoData.url) 86 videoInstance.set('url', videoData.url)
@@ -88,6 +91,8 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
88 videoInstance.set('support', videoData.support) 91 videoInstance.set('support', videoData.support)
89 videoInstance.set('nsfw', videoData.nsfw) 92 videoInstance.set('nsfw', videoData.nsfw)
90 videoInstance.set('commentsEnabled', videoData.commentsEnabled) 93 videoInstance.set('commentsEnabled', videoData.commentsEnabled)
94 videoInstance.set('waitTranscoding', videoData.waitTranscoding)
95 videoInstance.set('state', videoData.state)
91 videoInstance.set('duration', videoData.duration) 96 videoInstance.set('duration', videoData.duration)
92 videoInstance.set('createdAt', videoData.createdAt) 97 videoInstance.set('createdAt', videoData.createdAt)
93 videoInstance.set('updatedAt', videoData.updatedAt) 98 videoInstance.set('updatedAt', videoData.updatedAt)
@@ -98,8 +103,8 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
98 await videoInstance.save(sequelizeOptions) 103 await videoInstance.save(sequelizeOptions)
99 104
100 // Don't block on request 105 // Don't block on request
101 generateThumbnailFromUrl(videoInstance, videoAttributesToUpdate.icon) 106 generateThumbnailFromUrl(videoInstance, videoObject.icon)
102 .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoAttributesToUpdate.id, { err })) 107 .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err }))
103 108
104 // Remove old video files 109 // Remove old video files
105 const videoFileDestroyTasks: Bluebird<void>[] = [] 110 const videoFileDestroyTasks: Bluebird<void>[] = []
@@ -108,16 +113,16 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
108 } 113 }
109 await Promise.all(videoFileDestroyTasks) 114 await Promise.all(videoFileDestroyTasks)
110 115
111 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoInstance, videoAttributesToUpdate) 116 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoInstance, videoObject)
112 const tasks = videoFileAttributes.map(f => VideoFileModel.create(f)) 117 const tasks = videoFileAttributes.map(f => VideoFileModel.create(f))
113 await Promise.all(tasks) 118 await Promise.all(tasks)
114 119
115 const tags = videoAttributesToUpdate.tag.map(t => t.name) 120 const tags = videoObject.tag.map(t => t.name)
116 const tagInstances = await TagModel.findOrCreateTags(tags, t) 121 const tagInstances = await TagModel.findOrCreateTags(tags, t)
117 await videoInstance.$set('Tags', tagInstances, sequelizeOptions) 122 await videoInstance.$set('Tags', tagInstances, sequelizeOptions)
118 }) 123 })
119 124
120 logger.info('Remote video with uuid %s updated', videoAttributesToUpdate.uuid) 125 logger.info('Remote video with uuid %s updated', videoObject.uuid)
121 } catch (err) { 126 } catch (err) {
122 if (videoInstance !== undefined && videoFieldsSave !== undefined) { 127 if (videoInstance !== undefined && videoFieldsSave !== undefined) {
123 resetSequelizeInstance(videoInstance, videoFieldsSave) 128 resetSequelizeInstance(videoInstance, videoFieldsSave)
diff --git a/server/lib/activitypub/send/send-announce.ts b/server/lib/activitypub/send/send-announce.ts
index fa1d47259..dfc099ff2 100644
--- a/server/lib/activitypub/send/send-announce.ts
+++ b/server/lib/activitypub/send/send-announce.ts
@@ -11,7 +11,7 @@ async function buildVideoAnnounce (byActor: ActorModel, videoShare: VideoShareMo
11 11
12 const accountsToForwardView = await getActorsInvolvedInVideo(video, t) 12 const accountsToForwardView = await getActorsInvolvedInVideo(video, t)
13 const audience = getObjectFollowersAudience(accountsToForwardView) 13 const audience = getObjectFollowersAudience(accountsToForwardView)
14 return announceActivityData(videoShare.url, byActor, announcedObject, t, audience) 14 return announceActivityData(videoShare.url, byActor, announcedObject, audience)
15} 15}
16 16
17async function sendVideoAnnounce (byActor: ActorModel, videoShare: VideoShareModel, video: VideoModel, t: Transaction) { 17async function sendVideoAnnounce (byActor: ActorModel, videoShare: VideoShareModel, video: VideoModel, t: Transaction) {
@@ -20,16 +20,8 @@ async function sendVideoAnnounce (byActor: ActorModel, videoShare: VideoShareMod
20 return broadcastToFollowers(data, byActor, [ byActor ], t) 20 return broadcastToFollowers(data, byActor, [ byActor ], t)
21} 21}
22 22
23async function announceActivityData ( 23function announceActivityData (url: string, byActor: ActorModel, object: string, audience?: ActivityAudience): ActivityAnnounce {
24 url: string, 24 if (!audience) audience = getAudience(byActor)
25 byActor: ActorModel,
26 object: string,
27 t: Transaction,
28 audience?: ActivityAudience
29): Promise<ActivityAnnounce> {
30 if (!audience) {
31 audience = await getAudience(byActor, t)
32 }
33 25
34 return { 26 return {
35 type: 'Announce', 27 type: 'Announce',
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts
index 3ef4fcd3b..293947b05 100644
--- a/server/lib/activitypub/send/send-create.ts
+++ b/server/lib/activitypub/send/send-create.ts
@@ -23,8 +23,8 @@ async function sendCreateVideo (video: VideoModel, t: Transaction) {
23 const byActor = video.VideoChannel.Account.Actor 23 const byActor = video.VideoChannel.Account.Actor
24 const videoObject = video.toActivityPubObject() 24 const videoObject = video.toActivityPubObject()
25 25
26 const audience = await getAudience(byActor, t, video.privacy === VideoPrivacy.PUBLIC) 26 const audience = getAudience(byActor, video.privacy === VideoPrivacy.PUBLIC)
27 const data = await createActivityData(video.url, byActor, videoObject, t, audience) 27 const data = createActivityData(video.url, byActor, videoObject, audience)
28 28
29 return broadcastToFollowers(data, byActor, [ byActor ], t) 29 return broadcastToFollowers(data, byActor, [ byActor ], t)
30} 30}
@@ -33,7 +33,7 @@ async function sendVideoAbuse (byActor: ActorModel, videoAbuse: VideoAbuseModel,
33 const url = getVideoAbuseActivityPubUrl(videoAbuse) 33 const url = getVideoAbuseActivityPubUrl(videoAbuse)
34 34
35 const audience = { to: [ video.VideoChannel.Account.Actor.url ], cc: [] } 35 const audience = { to: [ video.VideoChannel.Account.Actor.url ], cc: [] }
36 const data = await createActivityData(url, byActor, videoAbuse.toActivityPubObject(), t, audience) 36 const data = createActivityData(url, byActor, videoAbuse.toActivityPubObject(), audience)
37 37
38 return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl) 38 return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
39} 39}
@@ -57,7 +57,7 @@ async function sendCreateVideoComment (comment: VideoCommentModel, t: Transactio
57 audience = getObjectFollowersAudience(actorsInvolvedInComment.concat(parentsCommentActors)) 57 audience = getObjectFollowersAudience(actorsInvolvedInComment.concat(parentsCommentActors))
58 } 58 }
59 59
60 const data = await createActivityData(comment.url, byActor, commentObject, t, audience) 60 const data = createActivityData(comment.url, byActor, commentObject, audience)
61 61
62 // This was a reply, send it to the parent actors 62 // This was a reply, send it to the parent actors
63 const actorsException = [ byActor ] 63 const actorsException = [ byActor ]
@@ -82,14 +82,14 @@ async function sendCreateView (byActor: ActorModel, video: VideoModel, t: Transa
82 // Send to origin 82 // Send to origin
83 if (video.isOwned() === false) { 83 if (video.isOwned() === false) {
84 const audience = getVideoAudience(video, actorsInvolvedInVideo) 84 const audience = getVideoAudience(video, actorsInvolvedInVideo)
85 const data = await createActivityData(url, byActor, viewActivityData, t, audience) 85 const data = createActivityData(url, byActor, viewActivityData, audience)
86 86
87 return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl) 87 return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
88 } 88 }
89 89
90 // Send to followers 90 // Send to followers
91 const audience = getObjectFollowersAudience(actorsInvolvedInVideo) 91 const audience = getObjectFollowersAudience(actorsInvolvedInVideo)
92 const data = await createActivityData(url, byActor, viewActivityData, t, audience) 92 const data = createActivityData(url, byActor, viewActivityData, audience)
93 93
94 // Use the server actor to send the view 94 // Use the server actor to send the view
95 const serverActor = await getServerActor() 95 const serverActor = await getServerActor()
@@ -106,34 +106,31 @@ async function sendCreateDislike (byActor: ActorModel, video: VideoModel, t: Tra
106 // Send to origin 106 // Send to origin
107 if (video.isOwned() === false) { 107 if (video.isOwned() === false) {
108 const audience = getVideoAudience(video, actorsInvolvedInVideo) 108 const audience = getVideoAudience(video, actorsInvolvedInVideo)
109 const data = await createActivityData(url, byActor, dislikeActivityData, t, audience) 109 const data = createActivityData(url, byActor, dislikeActivityData, audience)
110 110
111 return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl) 111 return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
112 } 112 }
113 113
114 // Send to followers 114 // Send to followers
115 const audience = getObjectFollowersAudience(actorsInvolvedInVideo) 115 const audience = getObjectFollowersAudience(actorsInvolvedInVideo)
116 const data = await createActivityData(url, byActor, dislikeActivityData, t, audience) 116 const data = createActivityData(url, byActor, dislikeActivityData, audience)
117 117
118 const actorsException = [ byActor ] 118 const actorsException = [ byActor ]
119 return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, actorsException) 119 return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, actorsException)
120} 120}
121 121
122async function createActivityData (url: string, 122function createActivityData (url: string, byActor: ActorModel, object: any, audience?: ActivityAudience): ActivityCreate {
123 byActor: ActorModel, 123 if (!audience) audience = getAudience(byActor)
124 object: any, 124
125 t: Transaction, 125 return audiencify(
126 audience?: ActivityAudience): Promise<ActivityCreate> { 126 {
127 if (!audience) { 127 type: 'Create' as 'Create',
128 audience = await getAudience(byActor, t) 128 id: url + '/activity',
129 } 129 actor: byActor.url,
130 130 object: audiencify(object, audience)
131 return audiencify({ 131 },
132 type: 'Create' as 'Create', 132 audience
133 id: url + '/activity', 133 )
134 actor: byActor.url,
135 object: audiencify(object, audience)
136 }, audience)
137} 134}
138 135
139function createDislikeActivityData (byActor: ActorModel, video: VideoModel) { 136function createDislikeActivityData (byActor: ActorModel, video: VideoModel) {
diff --git a/server/lib/activitypub/send/send-like.ts b/server/lib/activitypub/send/send-like.ts
index ddeb1fcd2..37ee7c096 100644
--- a/server/lib/activitypub/send/send-like.ts
+++ b/server/lib/activitypub/send/send-like.ts
@@ -14,36 +14,31 @@ async function sendLike (byActor: ActorModel, video: VideoModel, t: Transaction)
14 // Send to origin 14 // Send to origin
15 if (video.isOwned() === false) { 15 if (video.isOwned() === false) {
16 const audience = getVideoAudience(video, accountsInvolvedInVideo) 16 const audience = getVideoAudience(video, accountsInvolvedInVideo)
17 const data = await likeActivityData(url, byActor, video, t, audience) 17 const data = likeActivityData(url, byActor, video, audience)
18 18
19 return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl) 19 return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
20 } 20 }
21 21
22 // Send to followers 22 // Send to followers
23 const audience = getObjectFollowersAudience(accountsInvolvedInVideo) 23 const audience = getObjectFollowersAudience(accountsInvolvedInVideo)
24 const data = await likeActivityData(url, byActor, video, t, audience) 24 const data = likeActivityData(url, byActor, video, audience)
25 25
26 const followersException = [ byActor ] 26 const followersException = [ byActor ]
27 return broadcastToFollowers(data, byActor, accountsInvolvedInVideo, t, followersException) 27 return broadcastToFollowers(data, byActor, accountsInvolvedInVideo, t, followersException)
28} 28}
29 29
30async function likeActivityData ( 30function likeActivityData (url: string, byActor: ActorModel, video: VideoModel, audience?: ActivityAudience): ActivityLike {
31 url: string, 31 if (!audience) audience = getAudience(byActor)
32 byActor: ActorModel, 32
33 video: VideoModel, 33 return audiencify(
34 t: Transaction, 34 {
35 audience?: ActivityAudience 35 type: 'Like' as 'Like',
36): Promise<ActivityLike> { 36 id: url,
37 if (!audience) { 37 actor: byActor.url,
38 audience = await getAudience(byActor, t) 38 object: video.url
39 } 39 },
40 40 audience
41 return audiencify({ 41 )
42 type: 'Like' as 'Like',
43 id: url,
44 actor: byActor.url,
45 object: video.url
46 }, audience)
47} 42}
48 43
49// --------------------------------------------------------------------------- 44// ---------------------------------------------------------------------------
diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts
index 9733e66dc..33c3d2429 100644
--- a/server/lib/activitypub/send/send-undo.ts
+++ b/server/lib/activitypub/send/send-undo.ts
@@ -27,7 +27,7 @@ async function sendUndoFollow (actorFollow: ActorFollowModel, t: Transaction) {
27 const undoUrl = getUndoActivityPubUrl(followUrl) 27 const undoUrl = getUndoActivityPubUrl(followUrl)
28 28
29 const object = followActivityData(followUrl, me, following) 29 const object = followActivityData(followUrl, me, following)
30 const data = await undoActivityData(undoUrl, me, object, t) 30 const data = undoActivityData(undoUrl, me, object)
31 31
32 return unicastTo(data, me, following.inboxUrl) 32 return unicastTo(data, me, following.inboxUrl)
33} 33}
@@ -37,18 +37,18 @@ async function sendUndoLike (byActor: ActorModel, video: VideoModel, t: Transact
37 const undoUrl = getUndoActivityPubUrl(likeUrl) 37 const undoUrl = getUndoActivityPubUrl(likeUrl)
38 38
39 const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t) 39 const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
40 const object = await likeActivityData(likeUrl, byActor, video, t) 40 const object = likeActivityData(likeUrl, byActor, video)
41 41
42 // Send to origin 42 // Send to origin
43 if (video.isOwned() === false) { 43 if (video.isOwned() === false) {
44 const audience = getVideoAudience(video, actorsInvolvedInVideo) 44 const audience = getVideoAudience(video, actorsInvolvedInVideo)
45 const data = await undoActivityData(undoUrl, byActor, object, t, audience) 45 const data = undoActivityData(undoUrl, byActor, object, audience)
46 46
47 return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl) 47 return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
48 } 48 }
49 49
50 const audience = getObjectFollowersAudience(actorsInvolvedInVideo) 50 const audience = getObjectFollowersAudience(actorsInvolvedInVideo)
51 const data = await undoActivityData(undoUrl, byActor, object, t, audience) 51 const data = undoActivityData(undoUrl, byActor, object, audience)
52 52
53 const followersException = [ byActor ] 53 const followersException = [ byActor ]
54 return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException) 54 return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException)
@@ -60,16 +60,16 @@ async function sendUndoDislike (byActor: ActorModel, video: VideoModel, t: Trans
60 60
61 const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t) 61 const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
62 const dislikeActivity = createDislikeActivityData(byActor, video) 62 const dislikeActivity = createDislikeActivityData(byActor, video)
63 const object = await createActivityData(dislikeUrl, byActor, dislikeActivity, t) 63 const object = createActivityData(dislikeUrl, byActor, dislikeActivity)
64 64
65 if (video.isOwned() === false) { 65 if (video.isOwned() === false) {
66 const audience = getVideoAudience(video, actorsInvolvedInVideo) 66 const audience = getVideoAudience(video, actorsInvolvedInVideo)
67 const data = await undoActivityData(undoUrl, byActor, object, t, audience) 67 const data = undoActivityData(undoUrl, byActor, object, audience)
68 68
69 return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl) 69 return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
70 } 70 }
71 71
72 const data = await undoActivityData(undoUrl, byActor, object, t) 72 const data = undoActivityData(undoUrl, byActor, object)
73 73
74 const followersException = [ byActor ] 74 const followersException = [ byActor ]
75 return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException) 75 return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException)
@@ -80,7 +80,7 @@ async function sendUndoAnnounce (byActor: ActorModel, videoShare: VideoShareMode
80 80
81 const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t) 81 const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
82 const object = await buildVideoAnnounce(byActor, videoShare, video, t) 82 const object = await buildVideoAnnounce(byActor, videoShare, video, t)
83 const data = await undoActivityData(undoUrl, byActor, object, t) 83 const data = undoActivityData(undoUrl, byActor, object)
84 84
85 const followersException = [ byActor ] 85 const followersException = [ byActor ]
86 return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException) 86 return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException)
@@ -97,21 +97,21 @@ export {
97 97
98// --------------------------------------------------------------------------- 98// ---------------------------------------------------------------------------
99 99
100async function undoActivityData ( 100function undoActivityData (
101 url: string, 101 url: string,
102 byActor: ActorModel, 102 byActor: ActorModel,
103 object: ActivityFollow | ActivityLike | ActivityCreate | ActivityAnnounce, 103 object: ActivityFollow | ActivityLike | ActivityCreate | ActivityAnnounce,
104 t: Transaction,
105 audience?: ActivityAudience 104 audience?: ActivityAudience
106): Promise<ActivityUndo> { 105): ActivityUndo {
107 if (!audience) { 106 if (!audience) audience = getAudience(byActor)
108 audience = await getAudience(byActor, t) 107
109 } 108 return audiencify(
110 109 {
111 return audiencify({ 110 type: 'Undo' as 'Undo',
112 type: 'Undo' as 'Undo', 111 id: url,
113 id: url, 112 actor: byActor.url,
114 actor: byActor.url, 113 object
115 object 114 },
116 }, audience) 115 audience
116 )
117} 117}
diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts
index d64b88343..2fd374ec6 100644
--- a/server/lib/activitypub/send/send-update.ts
+++ b/server/lib/activitypub/send/send-update.ts
@@ -15,9 +15,9 @@ async function sendUpdateVideo (video: VideoModel, t: Transaction) {
15 15
16 const url = getUpdateActivityPubUrl(video.url, video.updatedAt.toISOString()) 16 const url = getUpdateActivityPubUrl(video.url, video.updatedAt.toISOString())
17 const videoObject = video.toActivityPubObject() 17 const videoObject = video.toActivityPubObject()
18 const audience = await getAudience(byActor, t, video.privacy === VideoPrivacy.PUBLIC) 18 const audience = getAudience(byActor, video.privacy === VideoPrivacy.PUBLIC)
19 19
20 const data = await updateActivityData(url, byActor, videoObject, t, audience) 20 const data = updateActivityData(url, byActor, videoObject, audience)
21 21
22 const actorsInvolved = await VideoShareModel.loadActorsByShare(video.id, t) 22 const actorsInvolved = await VideoShareModel.loadActorsByShare(video.id, t)
23 actorsInvolved.push(byActor) 23 actorsInvolved.push(byActor)
@@ -30,8 +30,8 @@ async function sendUpdateActor (accountOrChannel: AccountModel | VideoChannelMod
30 30
31 const url = getUpdateActivityPubUrl(byActor.url, byActor.updatedAt.toISOString()) 31 const url = getUpdateActivityPubUrl(byActor.url, byActor.updatedAt.toISOString())
32 const accountOrChannelObject = accountOrChannel.toActivityPubObject() 32 const accountOrChannelObject = accountOrChannel.toActivityPubObject()
33 const audience = await getAudience(byActor, t) 33 const audience = getAudience(byActor)
34 const data = await updateActivityData(url, byActor, accountOrChannelObject, t, audience) 34 const data = updateActivityData(url, byActor, accountOrChannelObject, audience)
35 35
36 let actorsInvolved: ActorModel[] 36 let actorsInvolved: ActorModel[]
37 if (accountOrChannel instanceof AccountModel) { 37 if (accountOrChannel instanceof AccountModel) {
@@ -56,21 +56,17 @@ export {
56 56
57// --------------------------------------------------------------------------- 57// ---------------------------------------------------------------------------
58 58
59async function updateActivityData ( 59function updateActivityData (url: string, byActor: ActorModel, object: any, audience?: ActivityAudience): ActivityUpdate {
60 url: string, 60 if (!audience) audience = getAudience(byActor)
61 byActor: ActorModel,
62 object: any,
63 t: Transaction,
64 audience?: ActivityAudience
65): Promise<ActivityUpdate> {
66 if (!audience) {
67 audience = await getAudience(byActor, t)
68 }
69 61
70 return audiencify({ 62 return audiencify(
71 type: 'Update' as 'Update', 63 {
72 id: url, 64 type: 'Update' as 'Update',
73 actor: byActor.url, 65 id: url,
74 object: audiencify(object, audience) 66 actor: byActor.url,
75 }, audience) 67 object: audiencify(object, audience
68 )
69 },
70 audience
71 )
76} 72}
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index 907f7e11e..7ec8ca193 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -1,8 +1,9 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import * as sequelize from 'sequelize'
2import * as magnetUtil from 'magnet-uri' 3import * as magnetUtil from 'magnet-uri'
3import { join } from 'path' 4import { join } from 'path'
4import * as request from 'request' 5import * as request from 'request'
5import { ActivityIconObject } from '../../../shared/index' 6import { ActivityIconObject, VideoState } from '../../../shared/index'
6import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' 7import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
7import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos' 8import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos'
8import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' 9import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos'
@@ -21,6 +22,21 @@ import { VideoShareModel } from '../../models/video/video-share'
21import { getOrCreateActorAndServerAndModel } from './actor' 22import { getOrCreateActorAndServerAndModel } from './actor'
22import { addVideoComments } from './video-comments' 23import { addVideoComments } from './video-comments'
23import { crawlCollectionPage } from './crawl' 24import { crawlCollectionPage } from './crawl'
25import { sendCreateVideo, sendUpdateVideo } from './send'
26import { shareVideoByServerAndChannel } from './index'
27
28async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
29 // If the video is not private and published, we federate it
30 if (video.privacy !== VideoPrivacy.PRIVATE && video.state === VideoState.PUBLISHED) {
31 if (isNewVideo === true) {
32 // Now we'll add the video's meta data to our followers
33 await sendCreateVideo(video, transaction)
34 await shareVideoByServerAndChannel(video, transaction)
35 } else {
36 await sendUpdateVideo(video, transaction)
37 }
38 }
39}
24 40
25function fetchRemoteVideoPreview (video: VideoModel, reject: Function) { 41function fetchRemoteVideoPreview (video: VideoModel, reject: Function) {
26 const host = video.VideoChannel.Account.Actor.Server.host 42 const host = video.VideoChannel.Account.Actor.Server.host
@@ -55,9 +71,11 @@ function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject)
55 return doRequestAndSaveToFile(options, thumbnailPath) 71 return doRequestAndSaveToFile(options, thumbnailPath)
56} 72}
57 73
58async function videoActivityObjectToDBAttributes (videoChannel: VideoChannelModel, 74async function videoActivityObjectToDBAttributes (
59 videoObject: VideoTorrentObject, 75 videoChannel: VideoChannelModel,
60 to: string[] = []) { 76 videoObject: VideoTorrentObject,
77 to: string[] = []
78) {
61 const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPrivacy.PUBLIC : VideoPrivacy.UNLISTED 79 const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPrivacy.PUBLIC : VideoPrivacy.UNLISTED
62 const duration = videoObject.duration.replace(/[^\d]+/, '') 80 const duration = videoObject.duration.replace(/[^\d]+/, '')
63 81
@@ -90,6 +108,8 @@ async function videoActivityObjectToDBAttributes (videoChannel: VideoChannelMode
90 support, 108 support,
91 nsfw: videoObject.sensitive, 109 nsfw: videoObject.sensitive,
92 commentsEnabled: videoObject.commentsEnabled, 110 commentsEnabled: videoObject.commentsEnabled,
111 waitTranscoding: videoObject.waitTranscoding,
112 state: videoObject.state,
93 channelId: videoChannel.id, 113 channelId: videoChannel.id,
94 duration: parseInt(duration, 10), 114 duration: parseInt(duration, 10),
95 createdAt: new Date(videoObject.published), 115 createdAt: new Date(videoObject.published),
@@ -185,22 +205,20 @@ async function getOrCreateVideo (videoObject: VideoTorrentObject, channelActor:
185} 205}
186 206
187async function getOrCreateAccountAndVideoAndChannel (videoObject: VideoTorrentObject | string, actor?: ActorModel) { 207async function getOrCreateAccountAndVideoAndChannel (videoObject: VideoTorrentObject | string, actor?: ActorModel) {
188 if (typeof videoObject === 'string') { 208 const videoUrl = typeof videoObject === 'string' ? videoObject : videoObject.id
189 const videoUrl = videoObject 209
190 210 const videoFromDatabase = await VideoModel.loadByUrlAndPopulateAccount(videoUrl)
191 const videoFromDatabase = await VideoModel.loadByUrlAndPopulateAccount(videoUrl) 211 if (videoFromDatabase) {
192 if (videoFromDatabase) { 212 return {
193 return { 213 video: videoFromDatabase,
194 video: videoFromDatabase, 214 actor: videoFromDatabase.VideoChannel.Account.Actor,
195 actor: videoFromDatabase.VideoChannel.Account.Actor, 215 channelActor: videoFromDatabase.VideoChannel.Actor
196 channelActor: videoFromDatabase.VideoChannel.Actor
197 }
198 } 216 }
199
200 videoObject = await fetchRemoteVideo(videoUrl)
201 if (!videoObject) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
202 } 217 }
203 218
219 videoObject = await fetchRemoteVideo(videoUrl)
220 if (!videoObject) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
221
204 if (!actor) { 222 if (!actor) {
205 const actorObj = videoObject.attributedTo.find(a => a.type === 'Person') 223 const actorObj = videoObject.attributedTo.find(a => a.type === 'Person')
206 if (!actorObj) throw new Error('Cannot find associated actor to video ' + videoObject.url) 224 if (!actorObj) throw new Error('Cannot find associated actor to video ' + videoObject.url)
@@ -291,20 +309,6 @@ async function addVideoShares (shareUrls: string[], instance: VideoModel) {
291 } 309 }
292} 310}
293 311
294export {
295 getOrCreateAccountAndVideoAndChannel,
296 fetchRemoteVideoPreview,
297 fetchRemoteVideoDescription,
298 generateThumbnailFromUrl,
299 videoActivityObjectToDBAttributes,
300 videoFileActivityUrlToDBAttributes,
301 getOrCreateVideo,
302 getOrCreateVideoChannel,
303 addVideoShares
304}
305
306// ---------------------------------------------------------------------------
307
308async function fetchRemoteVideo (videoUrl: string): Promise<VideoTorrentObject> { 312async function fetchRemoteVideo (videoUrl: string): Promise<VideoTorrentObject> {
309 const options = { 313 const options = {
310 uri: videoUrl, 314 uri: videoUrl,
@@ -324,3 +328,17 @@ async function fetchRemoteVideo (videoUrl: string): Promise<VideoTorrentObject>
324 328
325 return body 329 return body
326} 330}
331
332export {
333 federateVideoIfNeeded,
334 fetchRemoteVideo,
335 getOrCreateAccountAndVideoAndChannel,
336 fetchRemoteVideoPreview,
337 fetchRemoteVideoDescription,
338 generateThumbnailFromUrl,
339 videoActivityObjectToDBAttributes,
340 videoFileActivityUrlToDBAttributes,
341 getOrCreateVideo,
342 getOrCreateVideoChannel,
343 addVideoShares
344}
diff --git a/server/lib/job-queue/handlers/video-file.ts b/server/lib/job-queue/handlers/video-file.ts
index 85f7dbfc2..f5ad076a6 100644
--- a/server/lib/job-queue/handlers/video-file.ts
+++ b/server/lib/job-queue/handlers/video-file.ts
@@ -1,17 +1,16 @@
1import * as kue from 'kue' 1import * as kue from 'kue'
2import { VideoResolution } from '../../../../shared' 2import { VideoResolution, VideoState } from '../../../../shared'
3import { VideoPrivacy } from '../../../../shared/models/videos'
4import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
5import { computeResolutionsToTranscode } from '../../../helpers/utils' 4import { computeResolutionsToTranscode } from '../../../helpers/utils'
6import { sequelizeTypescript } from '../../../initializers'
7import { VideoModel } from '../../../models/video/video' 5import { VideoModel } from '../../../models/video/video'
8import { shareVideoByServerAndChannel } from '../../activitypub'
9import { sendCreateVideo, sendUpdateVideo } from '../../activitypub/send'
10import { JobQueue } from '../job-queue' 6import { JobQueue } from '../job-queue'
7import { federateVideoIfNeeded } from '../../activitypub'
8import { retryTransactionWrapper } from '../../../helpers/database-utils'
9import { sequelizeTypescript } from '../../../initializers'
11 10
12export type VideoFilePayload = { 11export type VideoFilePayload = {
13 videoUUID: string 12 videoUUID: string
14 isNewVideo: boolean 13 isNewVideo?: boolean
15 resolution?: VideoResolution 14 resolution?: VideoResolution
16 isPortraitMode?: boolean 15 isPortraitMode?: boolean
17} 16}
@@ -52,10 +51,20 @@ async function processVideoFile (job: kue.Job) {
52 // Transcoding in other resolution 51 // Transcoding in other resolution
53 if (payload.resolution) { 52 if (payload.resolution) {
54 await video.transcodeOriginalVideofile(payload.resolution, payload.isPortraitMode) 53 await video.transcodeOriginalVideofile(payload.resolution, payload.isPortraitMode)
55 await onVideoFileTranscoderOrImportSuccess(video) 54
55 const options = {
56 arguments: [ video ],
57 errorMessage: 'Cannot execute onVideoFileTranscoderOrImportSuccess with many retries.'
58 }
59 await retryTransactionWrapper(onVideoFileTranscoderOrImportSuccess, options)
56 } else { 60 } else {
57 await video.optimizeOriginalVideofile() 61 await video.optimizeOriginalVideofile()
58 await onVideoFileOptimizerSuccess(video, payload.isNewVideo) 62
63 const options = {
64 arguments: [ video, payload.isNewVideo ],
65 errorMessage: 'Cannot execute onVideoFileOptimizerSuccess with many retries.'
66 }
67 await retryTransactionWrapper(onVideoFileOptimizerSuccess, options)
59 } 68 }
60 69
61 return video 70 return video
@@ -64,68 +73,70 @@ async function processVideoFile (job: kue.Job) {
64async function onVideoFileTranscoderOrImportSuccess (video: VideoModel) { 73async function onVideoFileTranscoderOrImportSuccess (video: VideoModel) {
65 if (video === undefined) return undefined 74 if (video === undefined) return undefined
66 75
67 // Maybe the video changed in database, refresh it 76 return sequelizeTypescript.transaction(async t => {
68 const videoDatabase = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(video.uuid) 77 // Maybe the video changed in database, refresh it
69 // Video does not exist anymore 78 let videoDatabase = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(video.uuid, t)
70 if (!videoDatabase) return undefined 79 // Video does not exist anymore
80 if (!videoDatabase) return undefined
71 81
72 if (video.privacy !== VideoPrivacy.PRIVATE) { 82 // We transcoded the video file in another format, now we can publish it
73 await sendUpdateVideo(video, undefined) 83 const oldState = videoDatabase.state
74 } 84 videoDatabase.state = VideoState.PUBLISHED
85 videoDatabase = await videoDatabase.save({ transaction: t })
86
87 // If the video was not published, we consider it is a new one for other instances
88 const isNewVideo = oldState !== VideoState.PUBLISHED
89 await federateVideoIfNeeded(videoDatabase, isNewVideo, t)
75 90
76 return undefined 91 return undefined
92 })
77} 93}
78 94
79async function onVideoFileOptimizerSuccess (video: VideoModel, isNewVideo: boolean) { 95async function onVideoFileOptimizerSuccess (video: VideoModel, isNewVideo: boolean) {
80 if (video === undefined) return undefined 96 if (video === undefined) return undefined
81 97
82 // Maybe the video changed in database, refresh it 98 // Outside the transaction (IO on disk)
83 const videoDatabase = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(video.uuid) 99 const { videoFileResolution } = await video.getOriginalFileResolution()
84 // Video does not exist anymore 100
85 if (!videoDatabase) return undefined 101 return sequelizeTypescript.transaction(async t => {
86 102 // Maybe the video changed in database, refresh it
87 if (video.privacy !== VideoPrivacy.PRIVATE) { 103 const videoDatabase = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(video.uuid, t)
88 if (isNewVideo !== false) { 104 // Video does not exist anymore
89 // Now we'll add the video's meta data to our followers 105 if (!videoDatabase) return undefined
90 await sequelizeTypescript.transaction(async t => { 106
91 await sendCreateVideo(video, t) 107 // Create transcoding jobs if there are enabled resolutions
92 await shareVideoByServerAndChannel(video, t) 108 const resolutionsEnabled = computeResolutionsToTranscode(videoFileResolution)
93 }) 109 logger.info(
94 } else { 110 'Resolutions computed for video %s and origin file height of %d.', videoDatabase.uuid, videoFileResolution,
95 await sendUpdateVideo(video, undefined) 111 { resolutions: resolutionsEnabled }
96 } 112 )
97 } 113
98 114 if (resolutionsEnabled.length !== 0) {
99 const { videoFileResolution } = await videoDatabase.getOriginalFileResolution() 115 const tasks: Promise<any>[] = []
100 116
101 // Create transcoding jobs if there are enabled resolutions 117 for (const resolution of resolutionsEnabled) {
102 const resolutionsEnabled = computeResolutionsToTranscode(videoFileResolution) 118 const dataInput = {
103 logger.info( 119 videoUUID: videoDatabase.uuid,
104 'Resolutions computed for video %s and origin file height of %d.', videoDatabase.uuid, videoFileResolution, 120 resolution
105 { resolutions: resolutionsEnabled } 121 }
106 ) 122
123 const p = JobQueue.Instance.createJob({ type: 'video-file', payload: dataInput })
124 tasks.push(p)
125 }
107 126
108 if (resolutionsEnabled.length !== 0) { 127 await Promise.all(tasks)
109 const tasks: Promise<any>[] = []
110 128
111 for (const resolution of resolutionsEnabled) { 129 logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled })
112 const dataInput = { 130 } else {
113 videoUUID: videoDatabase.uuid, 131 // No transcoding to do, it's now published
114 resolution, 132 video.state = VideoState.PUBLISHED
115 isNewVideo 133 video = await video.save({ transaction: t })
116 }
117 134
118 const p = JobQueue.Instance.createJob({ type: 'video-file', payload: dataInput }) 135 logger.info('No transcoding jobs created for video %s (no resolutions).', video.uuid)
119 tasks.push(p)
120 } 136 }
121 137
122 await Promise.all(tasks) 138 return federateVideoIfNeeded(video, isNewVideo, t)
123 139 })
124 logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled })
125 } else {
126 logger.info('No transcoding jobs created for video %s (no resolutions enabled).')
127 return undefined
128 }
129} 140}
130 141
131// --------------------------------------------------------------------------- 142// ---------------------------------------------------------------------------
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts
index bdfa19b61..695fe0eea 100644
--- a/server/lib/job-queue/job-queue.ts
+++ b/server/lib/job-queue/job-queue.ts
@@ -79,6 +79,7 @@ class JobQueue {
79 const res = await handlers[ handlerName ](job) 79 const res = await handlers[ handlerName ](job)
80 return done(null, res) 80 return done(null, res)
81 } catch (err) { 81 } catch (err) {
82 logger.error('Cannot execute job %d.', job.id, { err })
82 return done(err) 83 return done(err)
83 } 84 }
84 }) 85 })