aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/activitypub/client.ts8
-rw-r--r--server/controllers/activitypub/outbox.ts4
-rw-r--r--server/controllers/api/users.ts7
-rw-r--r--server/controllers/api/videos/index.ts47
-rw-r--r--server/helpers/activitypub.ts26
-rw-r--r--server/helpers/custom-validators/activitypub/videos.ts6
-rw-r--r--server/helpers/custom-validators/videos.ts24
-rw-r--r--server/helpers/utils.ts22
-rw-r--r--server/initializers/constants.ts10
-rw-r--r--server/initializers/migrations/0220-video-state.ts62
-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
-rw-r--r--server/middlewares/cache.ts2
-rw-r--r--server/middlewares/validators/videos.ts10
-rw-r--r--server/models/video/video.ts132
-rw-r--r--server/tests/api/check-params/videos.ts15
-rw-r--r--server/tests/api/videos/multiple-servers.ts8
-rw-r--r--server/tests/api/videos/services.ts3
-rw-r--r--server/tests/api/videos/video-transcoder.ts74
-rw-r--r--server/tests/cli/create-transcoding-job.ts2
-rw-r--r--server/tests/utils/videos/videos.ts3
-rw-r--r--server/tools/import-videos.ts1
-rw-r--r--server/tools/upload.ts1
32 files changed, 523 insertions, 359 deletions
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts
index 1c780783c..ea8e25f68 100644
--- a/server/controllers/activitypub/client.ts
+++ b/server/controllers/activitypub/client.ts
@@ -123,11 +123,11 @@ async function accountFollowingController (req: express.Request, res: express.Re
123async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) { 123async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) {
124 const video: VideoModel = res.locals.video 124 const video: VideoModel = res.locals.video
125 125
126 const audience = await getAudience(video.VideoChannel.Account.Actor, undefined, video.privacy === VideoPrivacy.PUBLIC) 126 const audience = getAudience(video.VideoChannel.Account.Actor, video.privacy === VideoPrivacy.PUBLIC)
127 const videoObject = audiencify(video.toActivityPubObject(), audience) 127 const videoObject = audiencify(video.toActivityPubObject(), audience)
128 128
129 if (req.path.endsWith('/activity')) { 129 if (req.path.endsWith('/activity')) {
130 const data = await createActivityData(video.url, video.VideoChannel.Account.Actor, videoObject, undefined, audience) 130 const data = createActivityData(video.url, video.VideoChannel.Account.Actor, videoObject, audience)
131 return activityPubResponse(activityPubContextify(data), res) 131 return activityPubResponse(activityPubContextify(data), res)
132 } 132 }
133 133
@@ -210,12 +210,12 @@ async function videoCommentController (req: express.Request, res: express.Respon
210 210
211 const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined) 211 const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined)
212 const isPublic = true // Comments are always public 212 const isPublic = true // Comments are always public
213 const audience = await getAudience(videoComment.Account.Actor, undefined, isPublic) 213 const audience = getAudience(videoComment.Account.Actor, isPublic)
214 214
215 const videoCommentObject = audiencify(videoComment.toActivityPubObject(threadParentComments), audience) 215 const videoCommentObject = audiencify(videoComment.toActivityPubObject(threadParentComments), audience)
216 216
217 if (req.path.endsWith('/activity')) { 217 if (req.path.endsWith('/activity')) {
218 const data = await createActivityData(videoComment.url, videoComment.Account.Actor, videoCommentObject, undefined, audience) 218 const data = createActivityData(videoComment.url, videoComment.Account.Actor, videoCommentObject, audience)
219 return activityPubResponse(activityPubContextify(data), res) 219 return activityPubResponse(activityPubContextify(data), res)
220 } 220 }
221 221
diff --git a/server/controllers/activitypub/outbox.ts b/server/controllers/activitypub/outbox.ts
index 2793ae267..ae7adcd4c 100644
--- a/server/controllers/activitypub/outbox.ts
+++ b/server/controllers/activitypub/outbox.ts
@@ -54,12 +54,12 @@ async function buildActivities (actor: ActorModel, start: number, count: number)
54 // This is a shared video 54 // This is a shared video
55 if (video.VideoShares !== undefined && video.VideoShares.length !== 0) { 55 if (video.VideoShares !== undefined && video.VideoShares.length !== 0) {
56 const videoShare = video.VideoShares[0] 56 const videoShare = video.VideoShares[0]
57 const announceActivity = await announceActivityData(videoShare.url, actor, video.url, undefined, createActivityAudience) 57 const announceActivity = announceActivityData(videoShare.url, actor, video.url, createActivityAudience)
58 58
59 activities.push(announceActivity) 59 activities.push(announceActivity)
60 } else { 60 } else {
61 const videoObject = video.toActivityPubObject() 61 const videoObject = video.toActivityPubObject()
62 const createActivity = await createActivityData(video.url, byActor, videoObject, undefined, createActivityAudience) 62 const createActivity = createActivityData(video.url, byActor, videoObject, createActivityAudience)
63 63
64 activities.push(createActivity) 64 activities.push(createActivity)
65 } 65 }
diff --git a/server/controllers/api/users.ts b/server/controllers/api/users.ts
index 8dff4b87c..2b40c44d9 100644
--- a/server/controllers/api/users.ts
+++ b/server/controllers/api/users.ts
@@ -166,7 +166,7 @@ export {
166 166
167async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) { 167async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
168 const user = res.locals.oauth.token.User as UserModel 168 const user = res.locals.oauth.token.User as UserModel
169 const resultList = await VideoModel.listAccountVideosForApi( 169 const resultList = await VideoModel.listUserVideosForApi(
170 user.Account.id, 170 user.Account.id,
171 req.query.start as number, 171 req.query.start as number,
172 req.query.count as number, 172 req.query.count as number,
@@ -174,7 +174,8 @@ async function getUserVideos (req: express.Request, res: express.Response, next:
174 false // Display my NSFW videos 174 false // Display my NSFW videos
175 ) 175 )
176 176
177 return res.json(getFormattedObjects(resultList.data, resultList.total)) 177 const additionalAttributes = { waitTranscoding: true, state: true }
178 return res.json(getFormattedObjects(resultList.data, resultList.total, { additionalAttributes }))
178} 179}
179 180
180async function createUserRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { 181async function createUserRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) {
@@ -318,7 +319,7 @@ async function updateMe (req: express.Request, res: express.Response, next: expr
318} 319}
319 320
320async function updateMyAvatar (req: express.Request, res: express.Response, next: express.NextFunction) { 321async function updateMyAvatar (req: express.Request, res: express.Response, next: express.NextFunction) {
321 const avatarPhysicalFile = req.files['avatarfile'][0] 322 const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ]
322 const user = res.locals.oauth.token.user 323 const user = res.locals.oauth.token.user
323 const actor = user.Account.Actor 324 const actor = user.Account.Actor
324 325
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 7f5e74626..9d9b2b0e1 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -1,6 +1,6 @@
1import * as express from 'express' 1import * as express from 'express'
2import { extname, join } from 'path' 2import { extname, join } from 'path'
3import { VideoCreate, VideoPrivacy, VideoUpdate } from '../../../../shared' 3import { VideoCreate, VideoPrivacy, VideoState, VideoUpdate } from '../../../../shared'
4import { renamePromise } from '../../../helpers/core-utils' 4import { renamePromise } from '../../../helpers/core-utils'
5import { retryTransactionWrapper } from '../../../helpers/database-utils' 5import { retryTransactionWrapper } from '../../../helpers/database-utils'
6import { getVideoFileResolution } from '../../../helpers/ffmpeg-utils' 6import { getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
@@ -21,11 +21,11 @@ import {
21} from '../../../initializers' 21} from '../../../initializers'
22import { 22import {
23 changeVideoChannelShare, 23 changeVideoChannelShare,
24 federateVideoIfNeeded,
24 fetchRemoteVideoDescription, 25 fetchRemoteVideoDescription,
25 getVideoActivityPubUrl, 26 getVideoActivityPubUrl
26 shareVideoByServerAndChannel
27} from '../../../lib/activitypub' 27} from '../../../lib/activitypub'
28import { sendCreateVideo, sendCreateView, sendUpdateVideo } from '../../../lib/activitypub/send' 28import { sendCreateView } from '../../../lib/activitypub/send'
29import { JobQueue } from '../../../lib/job-queue' 29import { JobQueue } from '../../../lib/job-queue'
30import { Redis } from '../../../lib/redis' 30import { Redis } from '../../../lib/redis'
31import { 31import {
@@ -51,7 +51,7 @@ import { videoCommentRouter } from './comment'
51import { rateVideoRouter } from './rate' 51import { rateVideoRouter } from './rate'
52import { VideoFilter } from '../../../../shared/models/videos/video-query.type' 52import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
53import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type' 53import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type'
54import { isNSFWHidden, createReqFiles } from '../../../helpers/express-utils' 54import { createReqFiles, isNSFWHidden } from '../../../helpers/express-utils'
55 55
56const videosRouter = express.Router() 56const videosRouter = express.Router()
57 57
@@ -185,8 +185,10 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
185 category: videoInfo.category, 185 category: videoInfo.category,
186 licence: videoInfo.licence, 186 licence: videoInfo.licence,
187 language: videoInfo.language, 187 language: videoInfo.language,
188 commentsEnabled: videoInfo.commentsEnabled, 188 commentsEnabled: videoInfo.commentsEnabled || false,
189 nsfw: videoInfo.nsfw, 189 waitTranscoding: videoInfo.waitTranscoding || false,
190 state: CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED,
191 nsfw: videoInfo.nsfw || false,
190 description: videoInfo.description, 192 description: videoInfo.description,
191 support: videoInfo.support, 193 support: videoInfo.support,
192 privacy: videoInfo.privacy, 194 privacy: videoInfo.privacy,
@@ -194,19 +196,20 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
194 channelId: res.locals.videoChannel.id 196 channelId: res.locals.videoChannel.id
195 } 197 }
196 const video = new VideoModel(videoData) 198 const video = new VideoModel(videoData)
197 video.url = getVideoActivityPubUrl(video) 199 video.url = getVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
198 200
201 // Build the file object
199 const { videoFileResolution } = await getVideoFileResolution(videoPhysicalFile.path) 202 const { videoFileResolution } = await getVideoFileResolution(videoPhysicalFile.path)
200
201 const videoFileData = { 203 const videoFileData = {
202 extname: extname(videoPhysicalFile.filename), 204 extname: extname(videoPhysicalFile.filename),
203 resolution: videoFileResolution, 205 resolution: videoFileResolution,
204 size: videoPhysicalFile.size 206 size: videoPhysicalFile.size
205 } 207 }
206 const videoFile = new VideoFileModel(videoFileData) 208 const videoFile = new VideoFileModel(videoFileData)
209
210 // Move physical file
207 const videoDir = CONFIG.STORAGE.VIDEOS_DIR 211 const videoDir = CONFIG.STORAGE.VIDEOS_DIR
208 const destination = join(videoDir, video.getVideoFilename(videoFile)) 212 const destination = join(videoDir, video.getVideoFilename(videoFile))
209
210 await renamePromise(videoPhysicalFile.path, destination) 213 await renamePromise(videoPhysicalFile.path, destination)
211 // This is important in case if there is another attempt in the retry process 214 // This is important in case if there is another attempt in the retry process
212 videoPhysicalFile.filename = video.getVideoFilename(videoFile) 215 videoPhysicalFile.filename = video.getVideoFilename(videoFile)
@@ -230,6 +233,7 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
230 await video.createPreview(videoFile) 233 await video.createPreview(videoFile)
231 } 234 }
232 235
236 // Create the torrent file
233 await video.createTorrentAndSetInfoHash(videoFile) 237 await video.createTorrentAndSetInfoHash(videoFile)
234 238
235 const videoCreated = await sequelizeTypescript.transaction(async t => { 239 const videoCreated = await sequelizeTypescript.transaction(async t => {
@@ -251,20 +255,14 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
251 video.Tags = tagInstances 255 video.Tags = tagInstances
252 } 256 }
253 257
254 // Let transcoding job send the video to friends because the video file extension might change 258 await federateVideoIfNeeded(video, true, t)
255 if (CONFIG.TRANSCODING.ENABLED === true) return videoCreated
256 // Don't send video to remote servers, it is private
257 if (video.privacy === VideoPrivacy.PRIVATE) return videoCreated
258
259 await sendCreateVideo(video, t)
260 await shareVideoByServerAndChannel(video, t)
261 259
262 logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid) 260 logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid)
263 261
264 return videoCreated 262 return videoCreated
265 }) 263 })
266 264
267 if (CONFIG.TRANSCODING.ENABLED === true) { 265 if (video.state === VideoState.TO_TRANSCODE) {
268 // Put uuid because we don't have id auto incremented for now 266 // Put uuid because we don't have id auto incremented for now
269 const dataInput = { 267 const dataInput = {
270 videoUUID: videoCreated.uuid, 268 videoUUID: videoCreated.uuid,
@@ -318,6 +316,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
318 if (videoInfoToUpdate.licence !== undefined) videoInstance.set('licence', videoInfoToUpdate.licence) 316 if (videoInfoToUpdate.licence !== undefined) videoInstance.set('licence', videoInfoToUpdate.licence)
319 if (videoInfoToUpdate.language !== undefined) videoInstance.set('language', videoInfoToUpdate.language) 317 if (videoInfoToUpdate.language !== undefined) videoInstance.set('language', videoInfoToUpdate.language)
320 if (videoInfoToUpdate.nsfw !== undefined) videoInstance.set('nsfw', videoInfoToUpdate.nsfw) 318 if (videoInfoToUpdate.nsfw !== undefined) videoInstance.set('nsfw', videoInfoToUpdate.nsfw)
319 if (videoInfoToUpdate.waitTranscoding !== undefined) videoInstance.set('waitTranscoding', videoInfoToUpdate.waitTranscoding)
321 if (videoInfoToUpdate.support !== undefined) videoInstance.set('support', videoInfoToUpdate.support) 320 if (videoInfoToUpdate.support !== undefined) videoInstance.set('support', videoInfoToUpdate.support)
322 if (videoInfoToUpdate.description !== undefined) videoInstance.set('description', videoInfoToUpdate.description) 321 if (videoInfoToUpdate.description !== undefined) videoInstance.set('description', videoInfoToUpdate.description)
323 if (videoInfoToUpdate.commentsEnabled !== undefined) videoInstance.set('commentsEnabled', videoInfoToUpdate.commentsEnabled) 322 if (videoInfoToUpdate.commentsEnabled !== undefined) videoInstance.set('commentsEnabled', videoInfoToUpdate.commentsEnabled)
@@ -343,19 +342,13 @@ async function updateVideo (req: express.Request, res: express.Response) {
343 // Video channel update? 342 // Video channel update?
344 if (res.locals.videoChannel && videoInstanceUpdated.channelId !== res.locals.videoChannel.id) { 343 if (res.locals.videoChannel && videoInstanceUpdated.channelId !== res.locals.videoChannel.id) {
345 await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel, { transaction: t }) 344 await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel, { transaction: t })
346 videoInstance.VideoChannel = res.locals.videoChannel 345 videoInstanceUpdated.VideoChannel = res.locals.videoChannel
347 346
348 if (wasPrivateVideo === false) await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t) 347 if (wasPrivateVideo === false) await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t)
349 } 348 }
350 349
351 // Now we'll update the video's meta data to our friends 350 const isNewVideo = wasPrivateVideo && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE
352 if (wasPrivateVideo === false) await sendUpdateVideo(videoInstanceUpdated, t) 351 await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo)
353
354 // Video is not private anymore, send a create action to remote servers
355 if (wasPrivateVideo === true && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE) {
356 await sendCreateVideo(videoInstanceUpdated, t)
357 await shareVideoByServerAndChannel(videoInstanceUpdated, t)
358 }
359 }) 352 })
360 353
361 logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid) 354 logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid)
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts
index d1f3ec02d..37a251697 100644
--- a/server/helpers/activitypub.ts
+++ b/server/helpers/activitypub.ts
@@ -8,22 +8,24 @@ import { signObject } from './peertube-crypto'
8import { pageToStartAndCount } from './core-utils' 8import { pageToStartAndCount } from './core-utils'
9 9
10function activityPubContextify <T> (data: T) { 10function activityPubContextify <T> (data: T) {
11 return Object.assign(data,{ 11 return Object.assign(data, {
12 '@context': [ 12 '@context': [
13 'https://www.w3.org/ns/activitystreams', 13 'https://www.w3.org/ns/activitystreams',
14 'https://w3id.org/security/v1', 14 'https://w3id.org/security/v1',
15 { 15 {
16 'RsaSignature2017': 'https://w3id.org/security#RsaSignature2017', 16 RsaSignature2017: 'https://w3id.org/security#RsaSignature2017',
17 'Hashtag': 'as:Hashtag', 17 Hashtag: 'as:Hashtag',
18 'uuid': 'http://schema.org/identifier', 18 uuid: 'http://schema.org/identifier',
19 'category': 'http://schema.org/category', 19 category: 'http://schema.org/category',
20 'licence': 'http://schema.org/license', 20 licence: 'http://schema.org/license',
21 'sensitive': 'as:sensitive', 21 sensitive: 'as:sensitive',
22 'language': 'http://schema.org/inLanguage', 22 language: 'http://schema.org/inLanguage',
23 'views': 'http://schema.org/Number', 23 views: 'http://schema.org/Number',
24 'size': 'http://schema.org/Number', 24 stats: 'http://schema.org/Number',
25 'commentsEnabled': 'http://schema.org/Boolean', 25 size: 'http://schema.org/Number',
26 'support': 'http://schema.org/Text' 26 commentsEnabled: 'http://schema.org/Boolean',
27 waitTranscoding: 'http://schema.org/Boolean',
28 support: 'http://schema.org/Text'
27 }, 29 },
28 { 30 {
29 likes: { 31 likes: {
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts
index 7e1d57c34..37c90a0c8 100644
--- a/server/helpers/custom-validators/activitypub/videos.ts
+++ b/server/helpers/custom-validators/activitypub/videos.ts
@@ -6,11 +6,13 @@ import {
6 isVideoAbuseReasonValid, 6 isVideoAbuseReasonValid,
7 isVideoDurationValid, 7 isVideoDurationValid,
8 isVideoNameValid, 8 isVideoNameValid,
9 isVideoStateValid,
9 isVideoTagValid, 10 isVideoTagValid,
10 isVideoTruncatedDescriptionValid, 11 isVideoTruncatedDescriptionValid,
11 isVideoViewsValid 12 isVideoViewsValid
12} from '../videos' 13} from '../videos'
13import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' 14import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
15import { VideoState } from '../../../../shared/models/videos'
14 16
15function sanitizeAndCheckVideoTorrentCreateActivity (activity: any) { 17function sanitizeAndCheckVideoTorrentCreateActivity (activity: any) {
16 return isBaseActivityValid(activity, 'Create') && 18 return isBaseActivityValid(activity, 'Create') &&
@@ -50,6 +52,10 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
50 if (!setRemoteVideoTruncatedContent(video)) return false 52 if (!setRemoteVideoTruncatedContent(video)) return false
51 if (!setValidAttributedTo(video)) return false 53 if (!setValidAttributedTo(video)) return false
52 54
55 // Default attributes
56 if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED
57 if (!isBooleanValid(video.waitTranscoding)) video.waitTranscoding = false
58
53 return isActivityPubUrlValid(video.id) && 59 return isActivityPubUrlValid(video.id) &&
54 isVideoNameValid(video.name) && 60 isVideoNameValid(video.name) &&
55 isActivityPubVideoDurationValid(video.duration) && 61 isActivityPubVideoDurationValid(video.duration) &&
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts
index f365df985..8496e679a 100644
--- a/server/helpers/custom-validators/videos.ts
+++ b/server/helpers/custom-validators/videos.ts
@@ -10,7 +10,8 @@ import {
10 VIDEO_LICENCES, 10 VIDEO_LICENCES,
11 VIDEO_MIMETYPE_EXT, 11 VIDEO_MIMETYPE_EXT,
12 VIDEO_PRIVACIES, 12 VIDEO_PRIVACIES,
13 VIDEO_RATE_TYPES 13 VIDEO_RATE_TYPES,
14 VIDEO_STATES
14} from '../../initializers' 15} from '../../initializers'
15import { VideoModel } from '../../models/video/video' 16import { VideoModel } from '../../models/video/video'
16import { exists, isArray, isFileValid } from './misc' 17import { exists, isArray, isFileValid } from './misc'
@@ -21,11 +22,15 @@ const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS
21const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES 22const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES
22 23
23function isVideoCategoryValid (value: any) { 24function isVideoCategoryValid (value: any) {
24 return value === null || VIDEO_CATEGORIES[value] !== undefined 25 return value === null || VIDEO_CATEGORIES[ value ] !== undefined
26}
27
28function isVideoStateValid (value: any) {
29 return exists(value) && VIDEO_STATES[ value ] !== undefined
25} 30}
26 31
27function isVideoLicenceValid (value: any) { 32function isVideoLicenceValid (value: any) {
28 return value === null || VIDEO_LICENCES[value] !== undefined 33 return value === null || VIDEO_LICENCES[ value ] !== undefined
29} 34}
30 35
31function isVideoLanguageValid (value: any) { 36function isVideoLanguageValid (value: any) {
@@ -79,20 +84,22 @@ function isVideoRatingTypeValid (value: string) {
79 84
80const videoFileTypes = Object.keys(VIDEO_MIMETYPE_EXT).map(m => `(${m})`) 85const videoFileTypes = Object.keys(VIDEO_MIMETYPE_EXT).map(m => `(${m})`)
81const videoFileTypesRegex = videoFileTypes.join('|') 86const videoFileTypesRegex = videoFileTypes.join('|')
87
82function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { 88function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) {
83 return isFileValid(files, videoFileTypesRegex, 'videofile') 89 return isFileValid(files, videoFileTypesRegex, 'videofile')
84} 90}
85 91
86const videoImageTypes = CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME 92const videoImageTypes = CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME
87 .map(v => v.replace('.', '')) 93 .map(v => v.replace('.', ''))
88 .join('|') 94 .join('|')
89const videoImageTypesRegex = `image/(${videoImageTypes})` 95const videoImageTypesRegex = `image/(${videoImageTypes})`
96
90function isVideoImage (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], field: string) { 97function isVideoImage (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], field: string) {
91 return isFileValid(files, videoImageTypesRegex, field, true) 98 return isFileValid(files, videoImageTypesRegex, field, true)
92} 99}
93 100
94function isVideoPrivacyValid (value: string) { 101function isVideoPrivacyValid (value: string) {
95 return validator.isInt(value + '') && VIDEO_PRIVACIES[value] !== undefined 102 return validator.isInt(value + '') && VIDEO_PRIVACIES[ value ] !== undefined
96} 103}
97 104
98function isVideoFileInfoHashValid (value: string) { 105function isVideoFileInfoHashValid (value: string) {
@@ -118,8 +125,8 @@ async function isVideoExist (id: string, res: Response) {
118 125
119 if (!video) { 126 if (!video) {
120 res.status(404) 127 res.status(404)
121 .json({ error: 'Video not found' }) 128 .json({ error: 'Video not found' })
122 .end() 129 .end()
123 130
124 return false 131 return false
125 } 132 }
@@ -169,6 +176,7 @@ export {
169 isVideoTagsValid, 176 isVideoTagsValid,
170 isVideoAbuseReasonValid, 177 isVideoAbuseReasonValid,
171 isVideoFile, 178 isVideoFile,
179 isVideoStateValid,
172 isVideoViewsValid, 180 isVideoViewsValid,
173 isVideoRatingTypeValid, 181 isVideoRatingTypeValid,
174 isVideoDurationValid, 182 isVideoDurationValid,
diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts
index e4556fa12..8fa861281 100644
--- a/server/helpers/utils.ts
+++ b/server/helpers/utils.ts
@@ -1,6 +1,5 @@
1import { Model } from 'sequelize-typescript' 1import { Model } from 'sequelize-typescript'
2import * as ipaddr from 'ipaddr.js' 2import * as ipaddr from 'ipaddr.js'
3const isCidr = require('is-cidr')
4import { ResultList } from '../../shared' 3import { ResultList } from '../../shared'
5import { VideoResolution } from '../../shared/models/videos' 4import { VideoResolution } from '../../shared/models/videos'
6import { CONFIG } from '../initializers' 5import { CONFIG } from '../initializers'
@@ -10,6 +9,8 @@ import { ApplicationModel } from '../models/application/application'
10import { pseudoRandomBytesPromise } from './core-utils' 9import { pseudoRandomBytesPromise } from './core-utils'
11import { logger } from './logger' 10import { logger } from './logger'
12 11
12const isCidr = require('is-cidr')
13
13async function generateRandomString (size: number) { 14async function generateRandomString (size: number) {
14 const raw = await pseudoRandomBytesPromise(size) 15 const raw = await pseudoRandomBytesPromise(size)
15 16
@@ -17,22 +18,20 @@ async function generateRandomString (size: number) {
17} 18}
18 19
19interface FormattableToJSON { 20interface FormattableToJSON {
20 toFormattedJSON () 21 toFormattedJSON (args?: any)
21} 22}
22 23
23function getFormattedObjects<U, T extends FormattableToJSON> (objects: T[], objectsTotal: number) { 24function getFormattedObjects<U, T extends FormattableToJSON> (objects: T[], objectsTotal: number, formattedArg?: any) {
24 const formattedObjects: U[] = [] 25 const formattedObjects: U[] = []
25 26
26 objects.forEach(object => { 27 objects.forEach(object => {
27 formattedObjects.push(object.toFormattedJSON()) 28 formattedObjects.push(object.toFormattedJSON(formattedArg))
28 }) 29 })
29 30
30 const res: ResultList<U> = { 31 return {
31 total: objectsTotal, 32 total: objectsTotal,
32 data: formattedObjects 33 data: formattedObjects
33 } 34 } as ResultList<U>
34
35 return res
36} 35}
37 36
38async function isSignupAllowed () { 37async function isSignupAllowed () {
@@ -87,16 +86,17 @@ function computeResolutionsToTranscode (videoFileHeight: number) {
87 const resolutionsEnabled: number[] = [] 86 const resolutionsEnabled: number[] = []
88 const configResolutions = CONFIG.TRANSCODING.RESOLUTIONS 87 const configResolutions = CONFIG.TRANSCODING.RESOLUTIONS
89 88
89 // Put in the order we want to proceed jobs
90 const resolutions = [ 90 const resolutions = [
91 VideoResolution.H_240P,
92 VideoResolution.H_360P,
93 VideoResolution.H_480P, 91 VideoResolution.H_480P,
92 VideoResolution.H_360P,
94 VideoResolution.H_720P, 93 VideoResolution.H_720P,
94 VideoResolution.H_240P,
95 VideoResolution.H_1080P 95 VideoResolution.H_1080P
96 ] 96 ]
97 97
98 for (const resolution of resolutions) { 98 for (const resolution of resolutions) {
99 if (configResolutions[resolution + 'p'] === true && videoFileHeight > resolution) { 99 if (configResolutions[ resolution + 'p' ] === true && videoFileHeight > resolution) {
100 resolutionsEnabled.push(resolution) 100 resolutionsEnabled.push(resolution)
101 } 101 }
102 } 102 }
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 79e4bb7f0..8dbc1b060 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -1,6 +1,6 @@
1import { IConfig } from 'config' 1import { IConfig } from 'config'
2import { dirname, join } from 'path' 2import { dirname, join } from 'path'
3import { JobType, VideoRateType } from '../../shared/models' 3import { JobType, VideoRateType, VideoState } from '../../shared/models'
4import { ActivityPubActorType } from '../../shared/models/activitypub' 4import { ActivityPubActorType } from '../../shared/models/activitypub'
5import { FollowState } from '../../shared/models/actors' 5import { FollowState } from '../../shared/models/actors'
6import { VideoPrivacy } from '../../shared/models/videos' 6import { VideoPrivacy } from '../../shared/models/videos'
@@ -14,7 +14,7 @@ let config: IConfig = require('config')
14 14
15// --------------------------------------------------------------------------- 15// ---------------------------------------------------------------------------
16 16
17const LAST_MIGRATION_VERSION = 215 17const LAST_MIGRATION_VERSION = 220
18 18
19// --------------------------------------------------------------------------- 19// ---------------------------------------------------------------------------
20 20
@@ -326,6 +326,11 @@ const VIDEO_PRIVACIES = {
326 [VideoPrivacy.PRIVATE]: 'Private' 326 [VideoPrivacy.PRIVATE]: 'Private'
327} 327}
328 328
329const VIDEO_STATES = {
330 [VideoState.PUBLISHED]: 'Published',
331 [VideoState.TO_TRANSCODE]: 'To transcode'
332}
333
329const VIDEO_MIMETYPE_EXT = { 334const VIDEO_MIMETYPE_EXT = {
330 'video/webm': '.webm', 335 'video/webm': '.webm',
331 'video/ogg': '.ogv', 336 'video/ogg': '.ogv',
@@ -493,6 +498,7 @@ export {
493 VIDEO_LANGUAGES, 498 VIDEO_LANGUAGES,
494 VIDEO_PRIVACIES, 499 VIDEO_PRIVACIES,
495 VIDEO_LICENCES, 500 VIDEO_LICENCES,
501 VIDEO_STATES,
496 VIDEO_RATE_TYPES, 502 VIDEO_RATE_TYPES,
497 VIDEO_MIMETYPE_EXT, 503 VIDEO_MIMETYPE_EXT,
498 VIDEO_TRANSCODING_FPS, 504 VIDEO_TRANSCODING_FPS,
diff --git a/server/initializers/migrations/0220-video-state.ts b/server/initializers/migrations/0220-video-state.ts
new file mode 100644
index 000000000..491702157
--- /dev/null
+++ b/server/initializers/migrations/0220-video-state.ts
@@ -0,0 +1,62 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7}): Promise<void> {
8 // waitingTranscoding column
9 {
10 const data = {
11 type: Sequelize.BOOLEAN,
12 allowNull: true,
13 defaultValue: null
14 }
15 await utils.queryInterface.addColumn('video', 'waitTranscoding', data)
16 }
17
18 {
19 const query = 'UPDATE video SET "waitTranscoding" = false'
20 await utils.sequelize.query(query)
21 }
22
23 {
24 const data = {
25 type: Sequelize.BOOLEAN,
26 allowNull: false,
27 defaultValue: null
28 }
29 await utils.queryInterface.changeColumn('video', 'waitTranscoding', data)
30 }
31
32 // state
33 {
34 const data = {
35 type: Sequelize.INTEGER,
36 allowNull: true,
37 defaultValue: null
38 }
39 await utils.queryInterface.addColumn('video', 'state', data)
40 }
41
42 {
43 // Published
44 const query = 'UPDATE video SET "state" = 1'
45 await utils.sequelize.query(query)
46 }
47
48 {
49 const data = {
50 type: Sequelize.INTEGER,
51 allowNull: false,
52 defaultValue: null
53 }
54 await utils.queryInterface.changeColumn('video', 'state', data)
55 }
56}
57
58function down (options) {
59 throw new Error('Not implemented.')
60}
61
62export { up, down }
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 })
diff --git a/server/middlewares/cache.ts b/server/middlewares/cache.ts
index bf6659687..1de44db70 100644
--- a/server/middlewares/cache.ts
+++ b/server/middlewares/cache.ts
@@ -14,7 +14,7 @@ function cacheRoute (lifetime: number) {
14 14
15 // Not cached 15 // Not cached
16 if (!cached) { 16 if (!cached) {
17 logger.debug('Not cached result for route %s.', req.originalUrl) 17 logger.debug('No cached results for route %s.', req.originalUrl)
18 18
19 const sendSave = res.send.bind(res) 19 const sendSave = res.send.bind(res)
20 20
diff --git a/server/middlewares/validators/videos.ts b/server/middlewares/validators/videos.ts
index c5c45fe58..e181aebdb 100644
--- a/server/middlewares/validators/videos.ts
+++ b/server/middlewares/validators/videos.ts
@@ -55,8 +55,13 @@ const videosAddValidator = [
55 .customSanitizer(toValueOrNull) 55 .customSanitizer(toValueOrNull)
56 .custom(isVideoLanguageValid).withMessage('Should have a valid language'), 56 .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
57 body('nsfw') 57 body('nsfw')
58 .optional()
58 .toBoolean() 59 .toBoolean()
59 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'), 60 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
61 body('waitTranscoding')
62 .optional()
63 .toBoolean()
64 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
60 body('description') 65 body('description')
61 .optional() 66 .optional()
62 .customSanitizer(toValueOrNull) 67 .customSanitizer(toValueOrNull)
@@ -70,6 +75,7 @@ const videosAddValidator = [
70 .customSanitizer(toValueOrNull) 75 .customSanitizer(toValueOrNull)
71 .custom(isVideoTagsValid).withMessage('Should have correct tags'), 76 .custom(isVideoTagsValid).withMessage('Should have correct tags'),
72 body('commentsEnabled') 77 body('commentsEnabled')
78 .optional()
73 .toBoolean() 79 .toBoolean()
74 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'), 80 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
75 body('privacy') 81 body('privacy')
@@ -149,6 +155,10 @@ const videosUpdateValidator = [
149 .optional() 155 .optional()
150 .toBoolean() 156 .toBoolean()
151 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'), 157 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
158 body('waitTranscoding')
159 .optional()
160 .toBoolean()
161 .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
152 body('privacy') 162 body('privacy')
153 .optional() 163 .optional()
154 .toInt() 164 .toInt()
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 1cb1e6798..59c378efa 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -25,7 +25,7 @@ import {
25 Table, 25 Table,
26 UpdatedAt 26 UpdatedAt
27} from 'sequelize-typescript' 27} from 'sequelize-typescript'
28import { VideoPrivacy, VideoResolution } from '../../../shared' 28import { VideoPrivacy, VideoResolution, VideoState } from '../../../shared'
29import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' 29import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
30import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' 30import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
31import { VideoFilter } from '../../../shared/models/videos/video-query.type' 31import { VideoFilter } from '../../../shared/models/videos/video-query.type'
@@ -47,7 +47,7 @@ import {
47 isVideoLanguageValid, 47 isVideoLanguageValid,
48 isVideoLicenceValid, 48 isVideoLicenceValid,
49 isVideoNameValid, 49 isVideoNameValid,
50 isVideoPrivacyValid, 50 isVideoPrivacyValid, isVideoStateValid,
51 isVideoSupportValid 51 isVideoSupportValid
52} from '../../helpers/custom-validators/videos' 52} from '../../helpers/custom-validators/videos'
53import { generateImageFromVideoFile, getVideoFileResolution, transcode } from '../../helpers/ffmpeg-utils' 53import { generateImageFromVideoFile, getVideoFileResolution, transcode } from '../../helpers/ffmpeg-utils'
@@ -66,7 +66,7 @@ import {
66 VIDEO_EXT_MIMETYPE, 66 VIDEO_EXT_MIMETYPE,
67 VIDEO_LANGUAGES, 67 VIDEO_LANGUAGES,
68 VIDEO_LICENCES, 68 VIDEO_LICENCES,
69 VIDEO_PRIVACIES 69 VIDEO_PRIVACIES, VIDEO_STATES
70} from '../../initializers' 70} from '../../initializers'
71import { 71import {
72 getVideoCommentsActivityPubUrl, 72 getVideoCommentsActivityPubUrl,
@@ -93,10 +93,7 @@ enum ScopeNames {
93 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', 93 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
94 WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS', 94 WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS',
95 WITH_TAGS = 'WITH_TAGS', 95 WITH_TAGS = 'WITH_TAGS',
96 WITH_FILES = 'WITH_FILES', 96 WITH_FILES = 'WITH_FILES'
97 WITH_SHARES = 'WITH_SHARES',
98 WITH_RATES = 'WITH_RATES',
99 WITH_COMMENTS = 'WITH_COMMENTS'
100} 97}
101 98
102@Scopes({ 99@Scopes({
@@ -183,7 +180,20 @@ enum ScopeNames {
183 ')' 180 ')'
184 ) 181 )
185 }, 182 },
186 privacy: VideoPrivacy.PUBLIC 183 // Always list public videos
184 privacy: VideoPrivacy.PUBLIC,
185 // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding
186 [ Sequelize.Op.or ]: [
187 {
188 state: VideoState.PUBLISHED
189 },
190 {
191 [ Sequelize.Op.and ]: {
192 state: VideoState.TO_TRANSCODE,
193 waitTranscoding: false
194 }
195 }
196 ]
187 }, 197 },
188 include: [ videoChannelInclude ] 198 include: [ videoChannelInclude ]
189 } 199 }
@@ -272,42 +282,6 @@ enum ScopeNames {
272 required: true 282 required: true
273 } 283 }
274 ] 284 ]
275 },
276 [ScopeNames.WITH_SHARES]: {
277 include: [
278 {
279 ['separate' as any]: true,
280 model: () => VideoShareModel.unscoped()
281 }
282 ]
283 },
284 [ScopeNames.WITH_RATES]: {
285 include: [
286 {
287 ['separate' as any]: true,
288 model: () => AccountVideoRateModel,
289 include: [
290 {
291 model: () => AccountModel.unscoped(),
292 required: true,
293 include: [
294 {
295 attributes: [ 'url' ],
296 model: () => ActorModel.unscoped()
297 }
298 ]
299 }
300 ]
301 }
302 ]
303 },
304 [ScopeNames.WITH_COMMENTS]: {
305 include: [
306 {
307 ['separate' as any]: true,
308 model: () => VideoCommentModel.unscoped()
309 }
310 ]
311 } 285 }
312}) 286})
313@Table({ 287@Table({
@@ -335,7 +309,7 @@ enum ScopeNames {
335 fields: [ 'channelId' ] 309 fields: [ 'channelId' ]
336 }, 310 },
337 { 311 {
338 fields: [ 'id', 'privacy' ] 312 fields: [ 'id', 'privacy', 'state', 'waitTranscoding' ]
339 }, 313 },
340 { 314 {
341 fields: [ 'url'], 315 fields: [ 'url'],
@@ -435,6 +409,16 @@ export class VideoModel extends Model<VideoModel> {
435 @Column 409 @Column
436 commentsEnabled: boolean 410 commentsEnabled: boolean
437 411
412 @AllowNull(false)
413 @Column
414 waitTranscoding: boolean
415
416 @AllowNull(false)
417 @Default(null)
418 @Is('VideoState', value => throwIfNotValid(value, isVideoStateValid, 'state'))
419 @Column
420 state: VideoState
421
438 @CreatedAt 422 @CreatedAt
439 createdAt: Date 423 createdAt: Date
440 424
@@ -671,7 +655,7 @@ export class VideoModel extends Model<VideoModel> {
671 }) 655 })
672 } 656 }
673 657
674 static listAccountVideosForApi (accountId: number, start: number, count: number, sort: string, hideNSFW: boolean, withFiles = false) { 658 static listUserVideosForApi (accountId: number, start: number, count: number, sort: string, hideNSFW: boolean, withFiles = false) {
675 const query: IFindOptions<VideoModel> = { 659 const query: IFindOptions<VideoModel> = {
676 offset: start, 660 offset: start,
677 limit: count, 661 limit: count,
@@ -858,12 +842,13 @@ export class VideoModel extends Model<VideoModel> {
858 .findOne(options) 842 .findOne(options)
859 } 843 }
860 844
861 static loadByUUIDAndPopulateAccountAndServerAndTags (uuid: string) { 845 static loadByUUIDAndPopulateAccountAndServerAndTags (uuid: string, t?: Sequelize.Transaction) {
862 const options = { 846 const options = {
863 order: [ [ 'Tags', 'name', 'ASC' ] ], 847 order: [ [ 'Tags', 'name', 'ASC' ] ],
864 where: { 848 where: {
865 uuid 849 uuid
866 } 850 },
851 transaction: t
867 } 852 }
868 853
869 return VideoModel 854 return VideoModel
@@ -905,31 +890,23 @@ export class VideoModel extends Model<VideoModel> {
905 } 890 }
906 891
907 private static getCategoryLabel (id: number) { 892 private static getCategoryLabel (id: number) {
908 let categoryLabel = VIDEO_CATEGORIES[id] 893 return VIDEO_CATEGORIES[id] || 'Misc'
909 if (!categoryLabel) categoryLabel = 'Misc'
910
911 return categoryLabel
912 } 894 }
913 895
914 private static getLicenceLabel (id: number) { 896 private static getLicenceLabel (id: number) {
915 let licenceLabel = VIDEO_LICENCES[id] 897 return VIDEO_LICENCES[id] || 'Unknown'
916 if (!licenceLabel) licenceLabel = 'Unknown'
917
918 return licenceLabel
919 } 898 }
920 899
921 private static getLanguageLabel (id: string) { 900 private static getLanguageLabel (id: string) {
922 let languageLabel = VIDEO_LANGUAGES[id] 901 return VIDEO_LANGUAGES[id] || 'Unknown'
923 if (!languageLabel) languageLabel = 'Unknown'
924
925 return languageLabel
926 } 902 }
927 903
928 private static getPrivacyLabel (id: number) { 904 private static getPrivacyLabel (id: number) {
929 let privacyLabel = VIDEO_PRIVACIES[id] 905 return VIDEO_PRIVACIES[id] || 'Unknown'
930 if (!privacyLabel) privacyLabel = 'Unknown' 906 }
931 907
932 return privacyLabel 908 private static getStateLabel (id: number) {
909 return VIDEO_STATES[id] || 'Unknown'
933 } 910 }
934 911
935 getOriginalFile () { 912 getOriginalFile () {
@@ -1026,11 +1003,16 @@ export class VideoModel extends Model<VideoModel> {
1026 return join(STATIC_PATHS.PREVIEWS, this.getPreviewName()) 1003 return join(STATIC_PATHS.PREVIEWS, this.getPreviewName())
1027 } 1004 }
1028 1005
1029 toFormattedJSON (): Video { 1006 toFormattedJSON (options?: {
1007 additionalAttributes: {
1008 state: boolean,
1009 waitTranscoding: boolean
1010 }
1011 }): Video {
1030 const formattedAccount = this.VideoChannel.Account.toFormattedJSON() 1012 const formattedAccount = this.VideoChannel.Account.toFormattedJSON()
1031 const formattedVideoChannel = this.VideoChannel.toFormattedJSON() 1013 const formattedVideoChannel = this.VideoChannel.toFormattedJSON()
1032 1014
1033 return { 1015 const videoObject: Video = {
1034 id: this.id, 1016 id: this.id,
1035 uuid: this.uuid, 1017 uuid: this.uuid,
1036 name: this.name, 1018 name: this.name,
@@ -1082,6 +1064,19 @@ export class VideoModel extends Model<VideoModel> {
1082 avatar: formattedVideoChannel.avatar 1064 avatar: formattedVideoChannel.avatar
1083 } 1065 }
1084 } 1066 }
1067
1068 if (options) {
1069 if (options.additionalAttributes.state) {
1070 videoObject.state = {
1071 id: this.state,
1072 label: VideoModel.getStateLabel(this.state)
1073 }
1074 }
1075
1076 if (options.additionalAttributes.waitTranscoding) videoObject.waitTranscoding = this.waitTranscoding
1077 }
1078
1079 return videoObject
1085 } 1080 }
1086 1081
1087 toFormattedDetailsJSON (): VideoDetails { 1082 toFormattedDetailsJSON (): VideoDetails {
@@ -1094,6 +1089,11 @@ export class VideoModel extends Model<VideoModel> {
1094 account: this.VideoChannel.Account.toFormattedJSON(), 1089 account: this.VideoChannel.Account.toFormattedJSON(),
1095 tags: map(this.Tags, 'name'), 1090 tags: map(this.Tags, 'name'),
1096 commentsEnabled: this.commentsEnabled, 1091 commentsEnabled: this.commentsEnabled,
1092 waitTranscoding: this.waitTranscoding,
1093 state: {
1094 id: this.state,
1095 label: VideoModel.getStateLabel(this.state)
1096 },
1097 files: [] 1097 files: []
1098 } 1098 }
1099 1099
@@ -1207,6 +1207,8 @@ export class VideoModel extends Model<VideoModel> {
1207 language, 1207 language,
1208 views: this.views, 1208 views: this.views,
1209 sensitive: this.nsfw, 1209 sensitive: this.nsfw,
1210 waitTranscoding: this.waitTranscoding,
1211 state: this.state,
1210 commentsEnabled: this.commentsEnabled, 1212 commentsEnabled: this.commentsEnabled,
1211 published: this.publishedAt.toISOString(), 1213 published: this.publishedAt.toISOString(),
1212 updated: this.updatedAt.toISOString(), 1214 updated: this.updatedAt.toISOString(),
diff --git a/server/tests/api/check-params/videos.ts b/server/tests/api/check-params/videos.ts
index bc6c7fc46..04bed3b44 100644
--- a/server/tests/api/check-params/videos.ts
+++ b/server/tests/api/check-params/videos.ts
@@ -175,6 +175,7 @@ describe('Test videos API validator', function () {
175 language: 'pt', 175 language: 'pt',
176 nsfw: false, 176 nsfw: false,
177 commentsEnabled: true, 177 commentsEnabled: true,
178 waitTranscoding: true,
178 description: 'my super description', 179 description: 'my super description',
179 support: 'my super support text', 180 support: 'my super support text',
180 tags: [ 'tag1', 'tag2' ], 181 tags: [ 'tag1', 'tag2' ],
@@ -224,20 +225,6 @@ describe('Test videos API validator', function () {
224 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) 225 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
225 }) 226 })
226 227
227 it('Should fail without nsfw attribute', async function () {
228 const fields = omit(baseCorrectParams, 'nsfw')
229 const attaches = baseCorrectAttaches
230
231 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
232 })
233
234 it('Should fail without commentsEnabled attribute', async function () {
235 const fields = omit(baseCorrectParams, 'commentsEnabled')
236 const attaches = baseCorrectAttaches
237
238 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
239 })
240
241 it('Should fail with a long description', async function () { 228 it('Should fail with a long description', async function () {
242 const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(2500) }) 229 const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(2500) })
243 const attaches = baseCorrectAttaches 230 const attaches = baseCorrectAttaches
diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts
index 5f9a76621..edc46a644 100644
--- a/server/tests/api/videos/multiple-servers.ts
+++ b/server/tests/api/videos/multiple-servers.ts
@@ -924,7 +924,7 @@ describe('Test multiple servers', function () {
924 924
925 describe('With minimum parameters', function () { 925 describe('With minimum parameters', function () {
926 it('Should upload and propagate the video', async function () { 926 it('Should upload and propagate the video', async function () {
927 this.timeout(50000) 927 this.timeout(60000)
928 928
929 const path = '/api/v1/videos/upload' 929 const path = '/api/v1/videos/upload'
930 930
@@ -934,16 +934,14 @@ describe('Test multiple servers', function () {
934 .set('Authorization', 'Bearer ' + servers[1].accessToken) 934 .set('Authorization', 'Bearer ' + servers[1].accessToken)
935 .field('name', 'minimum parameters') 935 .field('name', 'minimum parameters')
936 .field('privacy', '1') 936 .field('privacy', '1')
937 .field('nsfw', 'false')
938 .field('channelId', '1') 937 .field('channelId', '1')
939 .field('commentsEnabled', 'true')
940 938
941 const filePath = join(__dirname, '..', '..', 'fixtures', 'video_short.webm') 939 const filePath = join(__dirname, '..', '..', 'fixtures', 'video_short.webm')
942 940
943 await req.attach('videofile', filePath) 941 await req.attach('videofile', filePath)
944 .expect(200) 942 .expect(200)
945 943
946 await wait(25000) 944 await wait(40000)
947 945
948 for (const server of servers) { 946 for (const server of servers) {
949 const res = await getVideosList(server.url) 947 const res = await getVideosList(server.url)
@@ -964,7 +962,7 @@ describe('Test multiple servers', function () {
964 }, 962 },
965 isLocal, 963 isLocal,
966 duration: 5, 964 duration: 5,
967 commentsEnabled: true, 965 commentsEnabled: false,
968 tags: [ ], 966 tags: [ ],
969 privacy: VideoPrivacy.PUBLIC, 967 privacy: VideoPrivacy.PUBLIC,
970 channel: { 968 channel: {
diff --git a/server/tests/api/videos/services.ts b/server/tests/api/videos/services.ts
index 45b4a1a81..51db000a2 100644
--- a/server/tests/api/videos/services.ts
+++ b/server/tests/api/videos/services.ts
@@ -32,7 +32,8 @@ describe('Test services', function () {
32 const oembedUrl = 'http://localhost:9001/videos/watch/' + server.video.uuid 32 const oembedUrl = 'http://localhost:9001/videos/watch/' + server.video.uuid
33 33
34 const res = await getOEmbed(server.url, oembedUrl) 34 const res = await getOEmbed(server.url, oembedUrl)
35 const expectedHtml = `<iframe width="560" height="315" src="http://localhost:9001/videos/embed/${server.video.uuid}" ` + 35 const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' +
36 `src="http://localhost:9001/videos/embed/${server.video.uuid}" ` +
36 'frameborder="0" allowfullscreen></iframe>' 37 'frameborder="0" allowfullscreen></iframe>'
37 const expectedThumbnailUrl = 'http://localhost:9001/static/previews/' + server.video.uuid + '.jpg' 38 const expectedThumbnailUrl = 'http://localhost:9001/static/previews/' + server.video.uuid + '.jpg'
38 39
diff --git a/server/tests/api/videos/video-transcoder.ts b/server/tests/api/videos/video-transcoder.ts
index ef929960d..1eace6491 100644
--- a/server/tests/api/videos/video-transcoder.ts
+++ b/server/tests/api/videos/video-transcoder.ts
@@ -2,11 +2,22 @@
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { VideoDetails } from '../../../../shared/models/videos' 5import { VideoDetails, VideoState } from '../../../../shared/models/videos'
6import { getVideoFileFPS } from '../../../helpers/ffmpeg-utils' 6import { getVideoFileFPS } from '../../../helpers/ffmpeg-utils'
7import { 7import {
8 flushAndRunMultipleServers, flushTests, getVideo, getVideosList, killallServers, root, ServerInfo, setAccessTokensToServers, uploadVideo, 8 doubleFollow,
9 wait, webtorrentAdd 9 flushAndRunMultipleServers,
10 flushTests,
11 getMyVideos,
12 getVideo,
13 getVideosList,
14 killallServers,
15 root,
16 ServerInfo,
17 setAccessTokensToServers,
18 uploadVideo,
19 wait,
20 webtorrentAdd
10} from '../../utils' 21} from '../../utils'
11import { join } from 'path' 22import { join } from 'path'
12 23
@@ -109,6 +120,63 @@ describe('Test video transcoding', function () {
109 } 120 }
110 }) 121 })
111 122
123 it('Should wait transcoding before publishing the video', async function () {
124 this.timeout(80000)
125
126 await doubleFollow(servers[0], servers[1])
127
128 await wait(15000)
129
130 {
131 // Upload the video, but wait transcoding
132 const videoAttributes = {
133 name: 'waiting video',
134 fixture: 'video_short1.webm',
135 waitTranscoding: true
136 }
137 const resVideo = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, videoAttributes)
138 const videoId = resVideo.body.video.uuid
139
140 // Should be in transcode state
141 const { body } = await getVideo(servers[ 1 ].url, videoId)
142 expect(body.name).to.equal('waiting video')
143 expect(body.state.id).to.equal(VideoState.TO_TRANSCODE)
144 expect(body.state.label).to.equal('To transcode')
145 expect(body.waitTranscoding).to.be.true
146
147 // Should have my video
148 const resMyVideos = await getMyVideos(servers[1].url, servers[1].accessToken, 0, 10)
149 const videoToFindInMine = resMyVideos.body.data.find(v => v.name === 'waiting video')
150 expect(videoToFindInMine).not.to.be.undefined
151 expect(videoToFindInMine.state.id).to.equal(VideoState.TO_TRANSCODE)
152 expect(videoToFindInMine.state.label).to.equal('To transcode')
153 expect(videoToFindInMine.waitTranscoding).to.be.true
154
155 // Should not list this video
156 const resVideos = await getVideosList(servers[1].url)
157 const videoToFindInList = resVideos.body.data.find(v => v.name === 'waiting video')
158 expect(videoToFindInList).to.be.undefined
159
160 // Server 1 should not have the video yet
161 await getVideo(servers[0].url, videoId, 404)
162 }
163
164 await wait(30000)
165
166 for (const server of servers) {
167 const res = await getVideosList(server.url)
168 const videoToFind = res.body.data.find(v => v.name === 'waiting video')
169 expect(videoToFind).not.to.be.undefined
170
171 const res2 = await getVideo(server.url, videoToFind.id)
172 const videoDetails: VideoDetails = res2.body
173
174 expect(videoDetails.state.id).to.equal(VideoState.PUBLISHED)
175 expect(videoDetails.state.label).to.equal('Published')
176 expect(videoDetails.waitTranscoding).to.be.true
177 }
178 })
179
112 after(async function () { 180 after(async function () {
113 killallServers(servers) 181 killallServers(servers)
114 182
diff --git a/server/tests/cli/create-transcoding-job.ts b/server/tests/cli/create-transcoding-job.ts
index 557dd8af9..fe1c0c03d 100644
--- a/server/tests/cli/create-transcoding-job.ts
+++ b/server/tests/cli/create-transcoding-job.ts
@@ -65,7 +65,7 @@ describe('Test create transcoding jobs', function () {
65 const env = getEnvCli(servers[0]) 65 const env = getEnvCli(servers[0])
66 await execCLI(`${env} npm run create-transcoding-job -- -v ${video2UUID}`) 66 await execCLI(`${env} npm run create-transcoding-job -- -v ${video2UUID}`)
67 67
68 await wait(30000) 68 await wait(40000)
69 69
70 for (const server of servers) { 70 for (const server of servers) {
71 const res = await getVideosList(server.url) 71 const res = await getVideosList(server.url)
diff --git a/server/tests/utils/videos/videos.ts b/server/tests/utils/videos/videos.ts
index ab0ce12ec..2c1d20ef1 100644
--- a/server/tests/utils/videos/videos.ts
+++ b/server/tests/utils/videos/videos.ts
@@ -27,6 +27,7 @@ type VideoAttributes = {
27 language?: string 27 language?: string
28 nsfw?: boolean 28 nsfw?: boolean
29 commentsEnabled?: boolean 29 commentsEnabled?: boolean
30 waitTranscoding?: boolean
30 description?: string 31 description?: string
31 tags?: string[] 32 tags?: string[]
32 channelId?: number 33 channelId?: number
@@ -326,6 +327,7 @@ async function uploadVideo (url: string, accessToken: string, videoAttributesArg
326 language: 'zh', 327 language: 'zh',
327 channelId: defaultChannelId, 328 channelId: defaultChannelId,
328 nsfw: true, 329 nsfw: true,
330 waitTranscoding: false,
329 description: 'my super description', 331 description: 'my super description',
330 support: 'my super support text', 332 support: 'my super support text',
331 tags: [ 'tag' ], 333 tags: [ 'tag' ],
@@ -341,6 +343,7 @@ async function uploadVideo (url: string, accessToken: string, videoAttributesArg
341 .field('name', attributes.name) 343 .field('name', attributes.name)
342 .field('nsfw', JSON.stringify(attributes.nsfw)) 344 .field('nsfw', JSON.stringify(attributes.nsfw))
343 .field('commentsEnabled', JSON.stringify(attributes.commentsEnabled)) 345 .field('commentsEnabled', JSON.stringify(attributes.commentsEnabled))
346 .field('waitTranscoding', JSON.stringify(attributes.waitTranscoding))
344 .field('privacy', attributes.privacy.toString()) 347 .field('privacy', attributes.privacy.toString())
345 .field('channelId', attributes.channelId) 348 .field('channelId', attributes.channelId)
346 349
diff --git a/server/tools/import-videos.ts b/server/tools/import-videos.ts
index fd351ae7e..e49fbb2f5 100644
--- a/server/tools/import-videos.ts
+++ b/server/tools/import-videos.ts
@@ -176,6 +176,7 @@ async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, languag
176 licence, 176 licence,
177 language, 177 language,
178 nsfw: isNSFW(videoInfo), 178 nsfw: isNSFW(videoInfo),
179 waitTranscoding: true,
179 commentsEnabled: true, 180 commentsEnabled: true,
180 description: videoInfo.description || undefined, 181 description: videoInfo.description || undefined,
181 support: undefined, 182 support: undefined,
diff --git a/server/tools/upload.ts b/server/tools/upload.ts
index 177d849f3..4d40c8c1a 100644
--- a/server/tools/upload.ts
+++ b/server/tools/upload.ts
@@ -84,6 +84,7 @@ async function run () {
84 fixture: program['file'], 84 fixture: program['file'],
85 thumbnailfile: program['thumbnailPath'], 85 thumbnailfile: program['thumbnailPath'],
86 previewfile: program['previewPath'], 86 previewfile: program['previewPath'],
87 waitTranscoding: true,
87 privacy: program['privacy'], 88 privacy: program['privacy'],
88 support: undefined 89 support: undefined
89 } 90 }