diff --git a/client/src/app/videos/+video-watch/video-watch.component.scss b/client/src/app/videos/+video-watch/video-watch.component.scss
index 00e776a69..06dd75653 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.scss
+++ b/client/src/app/videos/+video-watch/video-watch.component.scss
@@ -28,6 +28,10 @@
}
}
+#warning-transcoding {
+ text-align: center;
+}
+
#video-not-found {
height: 300px;
line-height: 300px;
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts
index eefa43a73..498542fff 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch.component.ts
@@ -1,5 +1,5 @@
import { catchError } from 'rxjs/operators'
-import { Component, ElementRef, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild, Inject } from '@angular/core'
+import { Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { RedirectService } from '@app/core/routing/redirect.service'
import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage'
@@ -10,7 +10,7 @@ import { Subscription } from 'rxjs'
import * as videojs from 'video.js'
import 'videojs-hotkeys'
import * as WebTorrent from 'webtorrent'
-import { UserVideoRateType, VideoRateType } from '../../../../../shared'
+import { UserVideoRateType, VideoRateType, VideoState } from '../../../../../shared'
import '../../../assets/player/peertube-videojs-plugin'
import { AuthService, ConfirmService } from '../../core'
import { RestExtractor, VideoBlacklistService } from '../../shared'
@@ -21,7 +21,7 @@ import { MarkdownService } from '../shared'
import { VideoDownloadComponent } from './modal/video-download.component'
import { VideoReportComponent } from './modal/video-report.component'
import { VideoShareComponent } from './modal/video-share.component'
-import { getVideojsOptions, loadLocale, addContextMenu } from '../../../assets/player/peertube-player'
+import { addContextMenu, getVideojsOptions, loadLocale } from '../../../assets/player/peertube-player'
import { ServerService } from '@app/core'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { environment } from '../../../environments/environment'
@@ -91,21 +91,21 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
}
this.videoService.getVideos({ currentPage: 1, itemsPerPage: 5 }, '-createdAt')
- .subscribe(
- data => {
- this.otherVideos = data.videos
- this.updateOtherVideosDisplayed()
- },
+ .subscribe(
+ data => {
+ this.otherVideos = data.videos
+ this.updateOtherVideosDisplayed()
+ },
- err => console.error(err)
- )
+ err => console.error(err)
+ )
this.paramsSub = this.route.params.subscribe(routeParams => {
if (this.player) {
this.player.pause()
}
- const uuid = routeParams['uuid']
+ const uuid = routeParams[ 'uuid' ]
// Video did not change
if (this.video && this.video.uuid === uuid) return
@@ -113,13 +113,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.videoService
.getVideo(uuid)
.pipe(catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 404 ])))
- .subscribe(
- video => {
- const startTime = this.route.snapshot.queryParams.start
- this.onVideoFetched(video, startTime)
- .catch(err => this.handleError(err))
- }
- )
+ .subscribe(video => {
+ const startTime = this.route.snapshot.queryParams.start
+ this.onVideoFetched(video, startTime)
+ .catch(err => this.handleError(err))
+ })
})
}
@@ -157,17 +155,17 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
if (res === false) return
this.videoBlacklistService.blacklistVideo(this.video.id)
- .subscribe(
- status => {
- this.notificationsService.success(
- this.i18n('Success'),
- this.i18n('Video {{videoName}} had been blacklisted.', { videoName: this.video.name })
- )
- this.redirectService.redirectToHomepage()
- },
+ .subscribe(
+ () => {
+ this.notificationsService.success(
+ this.i18n('Success'),
+ this.i18n('Video {{videoName}} had been blacklisted.', { videoName: this.video.name })
+ )
+ this.redirectService.redirectToHomepage()
+ },
- error => this.notificationsService.error(this.i18n('Error'), error.message)
- )
+ error => this.notificationsService.error(this.i18n('Error'), error.message)
+ )
}
showMoreDescription () {
@@ -188,22 +186,22 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.descriptionLoading = true
this.videoService.loadCompleteDescription(this.video.descriptionPath)
- .subscribe(
- description => {
- this.completeDescriptionShown = true
- this.descriptionLoading = false
+ .subscribe(
+ description => {
+ this.completeDescriptionShown = true
+ this.descriptionLoading = false
- this.shortVideoDescription = this.video.description
- this.completeVideoDescription = description
+ this.shortVideoDescription = this.video.description
+ this.completeVideoDescription = description
- this.updateVideoDescription(this.completeVideoDescription)
- },
+ this.updateVideoDescription(this.completeVideoDescription)
+ },
- error => {
- this.descriptionLoading = false
- this.notificationsService.error(this.i18n('Error'), error.message)
- }
- )
+ error => {
+ this.descriptionLoading = false
+ this.notificationsService.error(this.i18n('Error'), error.message)
+ }
+ )
}
showReportModal (event: Event) {
@@ -259,19 +257,19 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
if (res === false) return
this.videoService.removeVideo(this.video.id)
- .subscribe(
- status => {
- this.notificationsService.success(
- this.i18n('Success'),
- this.i18n('Video {{videoName}} deleted.', { videoName: this.video.name })
- )
+ .subscribe(
+ status => {
+ this.notificationsService.success(
+ this.i18n('Success'),
+ this.i18n('Video {{videoName}} deleted.', { videoName: this.video.name })
+ )
- // Go back to the video-list.
- this.redirectService.redirectToHomepage()
- },
+ // Go back to the video-list.
+ this.redirectService.redirectToHomepage()
+ },
- error => this.notificationsService.error(this.i18n('Error'), error.message)
- )
+ error => this.notificationsService.error(this.i18n('Error'), error.message)
+ )
}
acceptedPrivacyConcern () {
@@ -279,6 +277,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.hasAlreadyAcceptedPrivacyConcern = true
}
+ isVideoToTranscode () {
+ return this.video && this.video.state.id === VideoState.TO_TRANSCODE
+ }
+
private updateVideoDescription (description: string) {
this.video.description = description
this.setVideoDescriptionHTML()
@@ -294,10 +296,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
}
private setVideoLikesBarTooltipText () {
- this.likesBarTooltipText = this.i18n(
- '{{likesNumber}} likes / {{dislikesNumber}} dislikes',
- { likesNumber: this.video.likes, dislikesNumber: this.video.dislikes }
- )
+ this.likesBarTooltipText = this.i18n('{{likesNumber}} likes / {{dislikesNumber}} dislikes', {
+ likesNumber: this.video.likes,
+ dislikesNumber: this.video.dislikes
+ })
}
private handleError (err: any) {
@@ -320,15 +322,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
if (this.isUserLoggedIn() === false) return
this.videoService.getUserVideoRating(this.video.id)
- .subscribe(
- ratingObject => {
- if (ratingObject) {
- this.userRating = ratingObject.rating
- }
- },
+ .subscribe(
+ ratingObject => {
+ if (ratingObject) {
+ this.userRating = ratingObject.rating
+ }
+ },
- err => this.notificationsService.error(this.i18n('Error'), err.message)
- )
+ err => this.notificationsService.error(this.i18n('Error'), err.message)
+ )
}
private async onVideoFetched (video: VideoDetails, startTime = 0) {
@@ -409,14 +411,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
}
method.call(this.videoService, this.video.id)
- .subscribe(
- () => {
- // Update the video like attribute
- this.updateVideoRating(this.userRating, nextRating)
- this.userRating = nextRating
- },
- err => this.notificationsService.error(this.i18n('Error'), err.message)
- )
+ .subscribe(
+ () => {
+ // Update the video like attribute
+ this.updateVideoRating(this.userRating, nextRating)
+ this.userRating = nextRating
+ },
+
+ err => this.notificationsService.error(this.i18n('Error'), err.message)
+ )
}
private updateVideoRating (oldRating: UserVideoRateType, newRating: VideoRateType) {
diff --git a/package.json b/package.json
index 8d25613b6..739978a18 100644
--- a/package.json
+++ b/package.json
@@ -68,7 +68,6 @@
}
},
"lint-staged": {
- "*.{css,md}": "precise-commits",
"*.scss": [
"sass-lint -c .sass-lint.yml",
"git add"
@@ -166,7 +165,6 @@
"maildev": "^1.0.0-rc3",
"mocha": "^5.0.0",
"nodemon": "^1.11.0",
- "precise-commits": "^1.0.2",
"prettier": "1.13.2",
"prompt": "^1.0.0",
"sass-lint": "^1.12.1",
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
async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) {
const video: VideoModel = res.locals.video
- const audience = await getAudience(video.VideoChannel.Account.Actor, undefined, video.privacy === VideoPrivacy.PUBLIC)
+ const audience = getAudience(video.VideoChannel.Account.Actor, video.privacy === VideoPrivacy.PUBLIC)
const videoObject = audiencify(video.toActivityPubObject(), audience)
if (req.path.endsWith('/activity')) {
- const data = await createActivityData(video.url, video.VideoChannel.Account.Actor, videoObject, undefined, audience)
+ const data = createActivityData(video.url, video.VideoChannel.Account.Actor, videoObject, audience)
return activityPubResponse(activityPubContextify(data), res)
}
@@ -210,12 +210,12 @@ async function videoCommentController (req: express.Request, res: express.Respon
const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined)
const isPublic = true // Comments are always public
- const audience = await getAudience(videoComment.Account.Actor, undefined, isPublic)
+ const audience = getAudience(videoComment.Account.Actor, isPublic)
const videoCommentObject = audiencify(videoComment.toActivityPubObject(threadParentComments), audience)
if (req.path.endsWith('/activity')) {
- const data = await createActivityData(videoComment.url, videoComment.Account.Actor, videoCommentObject, undefined, audience)
+ const data = createActivityData(videoComment.url, videoComment.Account.Actor, videoCommentObject, audience)
return activityPubResponse(activityPubContextify(data), res)
}
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)
// This is a shared video
if (video.VideoShares !== undefined && video.VideoShares.length !== 0) {
const videoShare = video.VideoShares[0]
- const announceActivity = await announceActivityData(videoShare.url, actor, video.url, undefined, createActivityAudience)
+ const announceActivity = announceActivityData(videoShare.url, actor, video.url, createActivityAudience)
activities.push(announceActivity)
} else {
const videoObject = video.toActivityPubObject()
- const createActivity = await createActivityData(video.url, byActor, videoObject, undefined, createActivityAudience)
+ const createActivity = createActivityData(video.url, byActor, videoObject, createActivityAudience)
activities.push(createActivity)
}
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 {
async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
const user = res.locals.oauth.token.User as UserModel
- const resultList = await VideoModel.listAccountVideosForApi(
+ const resultList = await VideoModel.listUserVideosForApi(
user.Account.id,
req.query.start as number,
req.query.count as number,
@@ -174,7 +174,8 @@ async function getUserVideos (req: express.Request, res: express.Response, next:
false // Display my NSFW videos
)
- return res.json(getFormattedObjects(resultList.data, resultList.total))
+ const additionalAttributes = { waitTranscoding: true, state: true }
+ return res.json(getFormattedObjects(resultList.data, resultList.total, { additionalAttributes }))
}
async 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
}
async function updateMyAvatar (req: express.Request, res: express.Response, next: express.NextFunction) {
- const avatarPhysicalFile = req.files['avatarfile'][0]
+ const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ]
const user = res.locals.oauth.token.user
const actor = user.Account.Actor
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 @@
import * as express from 'express'
import { extname, join } from 'path'
-import { VideoCreate, VideoPrivacy, VideoUpdate } from '../../../../shared'
+import { VideoCreate, VideoPrivacy, VideoState, VideoUpdate } from '../../../../shared'
import { renamePromise } from '../../../helpers/core-utils'
import { retryTransactionWrapper } from '../../../helpers/database-utils'
import { getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
@@ -21,11 +21,11 @@ import {
} from '../../../initializers'
import {
changeVideoChannelShare,
+ federateVideoIfNeeded,
fetchRemoteVideoDescription,
- getVideoActivityPubUrl,
- shareVideoByServerAndChannel
+ getVideoActivityPubUrl
} from '../../../lib/activitypub'
-import { sendCreateVideo, sendCreateView, sendUpdateVideo } from '../../../lib/activitypub/send'
+import { sendCreateView } from '../../../lib/activitypub/send'
import { JobQueue } from '../../../lib/job-queue'
import { Redis } from '../../../lib/redis'
import {
@@ -51,7 +51,7 @@ import { videoCommentRouter } from './comment'
import { rateVideoRouter } from './rate'
import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type'
-import { isNSFWHidden, createReqFiles } from '../../../helpers/express-utils'
+import { createReqFiles, isNSFWHidden } from '../../../helpers/express-utils'
const videosRouter = express.Router()
@@ -185,8 +185,10 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
category: videoInfo.category,
licence: videoInfo.licence,
language: videoInfo.language,
- commentsEnabled: videoInfo.commentsEnabled,
- nsfw: videoInfo.nsfw,
+ commentsEnabled: videoInfo.commentsEnabled || false,
+ waitTranscoding: videoInfo.waitTranscoding || false,
+ state: CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED,
+ nsfw: videoInfo.nsfw || false,
description: videoInfo.description,
support: videoInfo.support,
privacy: videoInfo.privacy,
@@ -194,19 +196,20 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
channelId: res.locals.videoChannel.id
}
const video = new VideoModel(videoData)
- video.url = getVideoActivityPubUrl(video)
+ video.url = getVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
+ // Build the file object
const { videoFileResolution } = await getVideoFileResolution(videoPhysicalFile.path)
-
const videoFileData = {
extname: extname(videoPhysicalFile.filename),
resolution: videoFileResolution,
size: videoPhysicalFile.size
}
const videoFile = new VideoFileModel(videoFileData)
+
+ // Move physical file
const videoDir = CONFIG.STORAGE.VIDEOS_DIR
const destination = join(videoDir, video.getVideoFilename(videoFile))
-
await renamePromise(videoPhysicalFile.path, destination)
// This is important in case if there is another attempt in the retry process
videoPhysicalFile.filename = video.getVideoFilename(videoFile)
@@ -230,6 +233,7 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
await video.createPreview(videoFile)
}
+ // Create the torrent file
await video.createTorrentAndSetInfoHash(videoFile)
const videoCreated = await sequelizeTypescript.transaction(async t => {
@@ -251,20 +255,14 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
video.Tags = tagInstances
}
- // Let transcoding job send the video to friends because the video file extension might change
- if (CONFIG.TRANSCODING.ENABLED === true) return videoCreated
- // Don't send video to remote servers, it is private
- if (video.privacy === VideoPrivacy.PRIVATE) return videoCreated
-
- await sendCreateVideo(video, t)
- await shareVideoByServerAndChannel(video, t)
+ await federateVideoIfNeeded(video, true, t)
logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid)
return videoCreated
})
- if (CONFIG.TRANSCODING.ENABLED === true) {
+ if (video.state === VideoState.TO_TRANSCODE) {
// Put uuid because we don't have id auto incremented for now
const dataInput = {
videoUUID: videoCreated.uuid,
@@ -318,6 +316,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
if (videoInfoToUpdate.licence !== undefined) videoInstance.set('licence', videoInfoToUpdate.licence)
if (videoInfoToUpdate.language !== undefined) videoInstance.set('language', videoInfoToUpdate.language)
if (videoInfoToUpdate.nsfw !== undefined) videoInstance.set('nsfw', videoInfoToUpdate.nsfw)
+ if (videoInfoToUpdate.waitTranscoding !== undefined) videoInstance.set('waitTranscoding', videoInfoToUpdate.waitTranscoding)
if (videoInfoToUpdate.support !== undefined) videoInstance.set('support', videoInfoToUpdate.support)
if (videoInfoToUpdate.description !== undefined) videoInstance.set('description', videoInfoToUpdate.description)
if (videoInfoToUpdate.commentsEnabled !== undefined) videoInstance.set('commentsEnabled', videoInfoToUpdate.commentsEnabled)
@@ -343,19 +342,13 @@ async function updateVideo (req: express.Request, res: express.Response) {
// Video channel update?
if (res.locals.videoChannel && videoInstanceUpdated.channelId !== res.locals.videoChannel.id) {
await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel, { transaction: t })
- videoInstance.VideoChannel = res.locals.videoChannel
+ videoInstanceUpdated.VideoChannel = res.locals.videoChannel
if (wasPrivateVideo === false) await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t)
}
- // Now we'll update the video's meta data to our friends
- if (wasPrivateVideo === false) await sendUpdateVideo(videoInstanceUpdated, t)
-
- // Video is not private anymore, send a create action to remote servers
- if (wasPrivateVideo === true && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE) {
- await sendCreateVideo(videoInstanceUpdated, t)
- await shareVideoByServerAndChannel(videoInstanceUpdated, t)
- }
+ const isNewVideo = wasPrivateVideo && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE
+ await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo)
})
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'
import { pageToStartAndCount } from './core-utils'
function activityPubContextify
(data: T) {
- return Object.assign(data,{
+ return Object.assign(data, {
'@context': [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
{
- 'RsaSignature2017': 'https://w3id.org/security#RsaSignature2017',
- 'Hashtag': 'as:Hashtag',
- 'uuid': 'http://schema.org/identifier',
- 'category': 'http://schema.org/category',
- 'licence': 'http://schema.org/license',
- 'sensitive': 'as:sensitive',
- 'language': 'http://schema.org/inLanguage',
- 'views': 'http://schema.org/Number',
- 'size': 'http://schema.org/Number',
- 'commentsEnabled': 'http://schema.org/Boolean',
- 'support': 'http://schema.org/Text'
+ RsaSignature2017: 'https://w3id.org/security#RsaSignature2017',
+ Hashtag: 'as:Hashtag',
+ uuid: 'http://schema.org/identifier',
+ category: 'http://schema.org/category',
+ licence: 'http://schema.org/license',
+ sensitive: 'as:sensitive',
+ language: 'http://schema.org/inLanguage',
+ views: 'http://schema.org/Number',
+ stats: 'http://schema.org/Number',
+ size: 'http://schema.org/Number',
+ commentsEnabled: 'http://schema.org/Boolean',
+ waitTranscoding: 'http://schema.org/Boolean',
+ support: 'http://schema.org/Text'
},
{
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 {
isVideoAbuseReasonValid,
isVideoDurationValid,
isVideoNameValid,
+ isVideoStateValid,
isVideoTagValid,
isVideoTruncatedDescriptionValid,
isVideoViewsValid
} from '../videos'
import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
+import { VideoState } from '../../../../shared/models/videos'
function sanitizeAndCheckVideoTorrentCreateActivity (activity: any) {
return isBaseActivityValid(activity, 'Create') &&
@@ -50,6 +52,10 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
if (!setRemoteVideoTruncatedContent(video)) return false
if (!setValidAttributedTo(video)) return false
+ // Default attributes
+ if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED
+ if (!isBooleanValid(video.waitTranscoding)) video.waitTranscoding = false
+
return isActivityPubUrlValid(video.id) &&
isVideoNameValid(video.name) &&
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 {
VIDEO_LICENCES,
VIDEO_MIMETYPE_EXT,
VIDEO_PRIVACIES,
- VIDEO_RATE_TYPES
+ VIDEO_RATE_TYPES,
+ VIDEO_STATES
} from '../../initializers'
import { VideoModel } from '../../models/video/video'
import { exists, isArray, isFileValid } from './misc'
@@ -21,11 +22,15 @@ const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS
const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES
function isVideoCategoryValid (value: any) {
- return value === null || VIDEO_CATEGORIES[value] !== undefined
+ return value === null || VIDEO_CATEGORIES[ value ] !== undefined
+}
+
+function isVideoStateValid (value: any) {
+ return exists(value) && VIDEO_STATES[ value ] !== undefined
}
function isVideoLicenceValid (value: any) {
- return value === null || VIDEO_LICENCES[value] !== undefined
+ return value === null || VIDEO_LICENCES[ value ] !== undefined
}
function isVideoLanguageValid (value: any) {
@@ -79,20 +84,22 @@ function isVideoRatingTypeValid (value: string) {
const videoFileTypes = Object.keys(VIDEO_MIMETYPE_EXT).map(m => `(${m})`)
const videoFileTypesRegex = videoFileTypes.join('|')
+
function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) {
return isFileValid(files, videoFileTypesRegex, 'videofile')
}
const videoImageTypes = CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME
- .map(v => v.replace('.', ''))
- .join('|')
+ .map(v => v.replace('.', ''))
+ .join('|')
const videoImageTypesRegex = `image/(${videoImageTypes})`
+
function isVideoImage (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], field: string) {
return isFileValid(files, videoImageTypesRegex, field, true)
}
function isVideoPrivacyValid (value: string) {
- return validator.isInt(value + '') && VIDEO_PRIVACIES[value] !== undefined
+ return validator.isInt(value + '') && VIDEO_PRIVACIES[ value ] !== undefined
}
function isVideoFileInfoHashValid (value: string) {
@@ -118,8 +125,8 @@ async function isVideoExist (id: string, res: Response) {
if (!video) {
res.status(404)
- .json({ error: 'Video not found' })
- .end()
+ .json({ error: 'Video not found' })
+ .end()
return false
}
@@ -169,6 +176,7 @@ export {
isVideoTagsValid,
isVideoAbuseReasonValid,
isVideoFile,
+ isVideoStateValid,
isVideoViewsValid,
isVideoRatingTypeValid,
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 @@
import { Model } from 'sequelize-typescript'
import * as ipaddr from 'ipaddr.js'
-const isCidr = require('is-cidr')
import { ResultList } from '../../shared'
import { VideoResolution } from '../../shared/models/videos'
import { CONFIG } from '../initializers'
@@ -10,6 +9,8 @@ import { ApplicationModel } from '../models/application/application'
import { pseudoRandomBytesPromise } from './core-utils'
import { logger } from './logger'
+const isCidr = require('is-cidr')
+
async function generateRandomString (size: number) {
const raw = await pseudoRandomBytesPromise(size)
@@ -17,22 +18,20 @@ async function generateRandomString (size: number) {
}
interface FormattableToJSON {
- toFormattedJSON ()
+ toFormattedJSON (args?: any)
}
-function getFormattedObjects (objects: T[], objectsTotal: number) {
+function getFormattedObjects (objects: T[], objectsTotal: number, formattedArg?: any) {
const formattedObjects: U[] = []
objects.forEach(object => {
- formattedObjects.push(object.toFormattedJSON())
+ formattedObjects.push(object.toFormattedJSON(formattedArg))
})
- const res: ResultList = {
+ return {
total: objectsTotal,
data: formattedObjects
- }
-
- return res
+ } as ResultList
}
async function isSignupAllowed () {
@@ -87,16 +86,17 @@ function computeResolutionsToTranscode (videoFileHeight: number) {
const resolutionsEnabled: number[] = []
const configResolutions = CONFIG.TRANSCODING.RESOLUTIONS
+ // Put in the order we want to proceed jobs
const resolutions = [
- VideoResolution.H_240P,
- VideoResolution.H_360P,
VideoResolution.H_480P,
+ VideoResolution.H_360P,
VideoResolution.H_720P,
+ VideoResolution.H_240P,
VideoResolution.H_1080P
]
for (const resolution of resolutions) {
- if (configResolutions[resolution + 'p'] === true && videoFileHeight > resolution) {
+ if (configResolutions[ resolution + 'p' ] === true && videoFileHeight > resolution) {
resolutionsEnabled.push(resolution)
}
}
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 @@
import { IConfig } from 'config'
import { dirname, join } from 'path'
-import { JobType, VideoRateType } from '../../shared/models'
+import { JobType, VideoRateType, VideoState } from '../../shared/models'
import { ActivityPubActorType } from '../../shared/models/activitypub'
import { FollowState } from '../../shared/models/actors'
import { VideoPrivacy } from '../../shared/models/videos'
@@ -14,7 +14,7 @@ let config: IConfig = require('config')
// ---------------------------------------------------------------------------
-const LAST_MIGRATION_VERSION = 215
+const LAST_MIGRATION_VERSION = 220
// ---------------------------------------------------------------------------
@@ -326,6 +326,11 @@ const VIDEO_PRIVACIES = {
[VideoPrivacy.PRIVATE]: 'Private'
}
+const VIDEO_STATES = {
+ [VideoState.PUBLISHED]: 'Published',
+ [VideoState.TO_TRANSCODE]: 'To transcode'
+}
+
const VIDEO_MIMETYPE_EXT = {
'video/webm': '.webm',
'video/ogg': '.ogv',
@@ -493,6 +498,7 @@ export {
VIDEO_LANGUAGES,
VIDEO_PRIVACIES,
VIDEO_LICENCES,
+ VIDEO_STATES,
VIDEO_RATE_TYPES,
VIDEO_MIMETYPE_EXT,
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 @@
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+ transaction: Sequelize.Transaction
+ queryInterface: Sequelize.QueryInterface
+ sequelize: Sequelize.Sequelize
+}): Promise {
+ // waitingTranscoding column
+ {
+ const data = {
+ type: Sequelize.BOOLEAN,
+ allowNull: true,
+ defaultValue: null
+ }
+ await utils.queryInterface.addColumn('video', 'waitTranscoding', data)
+ }
+
+ {
+ const query = 'UPDATE video SET "waitTranscoding" = false'
+ await utils.sequelize.query(query)
+ }
+
+ {
+ const data = {
+ type: Sequelize.BOOLEAN,
+ allowNull: false,
+ defaultValue: null
+ }
+ await utils.queryInterface.changeColumn('video', 'waitTranscoding', data)
+ }
+
+ // state
+ {
+ const data = {
+ type: Sequelize.INTEGER,
+ allowNull: true,
+ defaultValue: null
+ }
+ await utils.queryInterface.addColumn('video', 'state', data)
+ }
+
+ {
+ // Published
+ const query = 'UPDATE video SET "state" = 1'
+ await utils.sequelize.query(query)
+ }
+
+ {
+ const data = {
+ type: Sequelize.INTEGER,
+ allowNull: false,
+ defaultValue: null
+ }
+ await utils.queryInterface.changeColumn('video', 'state', data)
+ }
+}
+
+function down (options) {
+ throw new Error('Not implemented.')
+}
+
+export { 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 (
isOrigin = false
) {
const to = [ ACTIVITY_PUB.PUBLIC ]
- const cc = [ ]
+ const cc = []
// Owner of the video we comment
if (isOrigin === false) {
@@ -55,7 +55,7 @@ async function getActorsInvolvedInVideo (video: VideoModel, t: Transaction) {
return actors
}
-async function getAudience (actorSender: ActorModel, t: Transaction, isPublic = true) {
+function getAudience (actorSender: ActorModel, isPublic = true) {
return buildAudience([ actorSender.followersUrl ], isPublic)
}
@@ -67,14 +67,14 @@ function buildAudience (followerUrls: string[], isPublic = true) {
to = [ ACTIVITY_PUB.PUBLIC ]
cc = followerUrls
} else { // Unlisted
- to = [ ]
- cc = [ ]
+ to = []
+ cc = []
}
return { to, cc }
}
-function audiencify (object: T, audience: ActivityAudience) {
+function audiencify (object: T, audience: ActivityAudience) {
return Object.assign(object, audience)
}
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 (uri: string, handler: (items: T[]) => Pr
if (Array.isArray(body.orderedItems)) {
const items = body.orderedItems
- logger.info('Processing %i ActivityPub items for %s.', items.length, nextLink)
+ logger.info('Processing %i ActivityPub items for %s.', items.length, options.uri)
await handler(items)
}
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 @@
import * as Bluebird from 'bluebird'
import { ActivityUpdate } from '../../../../shared/models/activitypub'
import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor'
-import { VideoTorrentObject } from '../../../../shared/models/activitypub/objects'
import { retryTransactionWrapper } from '../../../helpers/database-utils'
import { logger } from '../../../helpers/logger'
import { resetSequelizeInstance } from '../../../helpers/utils'
@@ -13,6 +12,7 @@ import { VideoChannelModel } from '../../../models/video/video-channel'
import { VideoFileModel } from '../../../models/video/video-file'
import { fetchAvatarIfExists, getOrCreateActorAndServerAndModel, updateActorAvatarInstance, updateActorInstance } from '../actor'
import {
+ fetchRemoteVideo,
generateThumbnailFromUrl,
getOrCreateAccountAndVideoAndChannel,
getOrCreateVideoChannel,
@@ -51,15 +51,18 @@ function processUpdateVideo (actor: ActorModel, activity: ActivityUpdate) {
}
async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
- const videoAttributesToUpdate = activity.object as VideoTorrentObject
+ const videoUrl = activity.object.id
- const res = await getOrCreateAccountAndVideoAndChannel(videoAttributesToUpdate.id)
+ const videoObject = await fetchRemoteVideo(videoUrl)
+ if (!videoObject) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
+
+ const res = await getOrCreateAccountAndVideoAndChannel(videoObject.id)
// Fetch video channel outside the transaction
- const newVideoChannelActor = await getOrCreateVideoChannel(videoAttributesToUpdate)
+ const newVideoChannelActor = await getOrCreateVideoChannel(videoObject)
const newVideoChannel = newVideoChannelActor.VideoChannel
- logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid)
+ logger.debug('Updating remote video "%s".', videoObject.uuid)
let videoInstance = res.video
let videoFieldsSave: any
@@ -77,7 +80,7 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
throw new Error('Account ' + actor.url + ' does not own video channel ' + videoChannel.Actor.url)
}
- const videoData = await videoActivityObjectToDBAttributes(newVideoChannel, videoAttributesToUpdate, activity.to)
+ const videoData = await videoActivityObjectToDBAttributes(newVideoChannel, videoObject, activity.to)
videoInstance.set('name', videoData.name)
videoInstance.set('uuid', videoData.uuid)
videoInstance.set('url', videoData.url)
@@ -88,6 +91,8 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
videoInstance.set('support', videoData.support)
videoInstance.set('nsfw', videoData.nsfw)
videoInstance.set('commentsEnabled', videoData.commentsEnabled)
+ videoInstance.set('waitTranscoding', videoData.waitTranscoding)
+ videoInstance.set('state', videoData.state)
videoInstance.set('duration', videoData.duration)
videoInstance.set('createdAt', videoData.createdAt)
videoInstance.set('updatedAt', videoData.updatedAt)
@@ -98,8 +103,8 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
await videoInstance.save(sequelizeOptions)
// Don't block on request
- generateThumbnailFromUrl(videoInstance, videoAttributesToUpdate.icon)
- .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoAttributesToUpdate.id, { err }))
+ generateThumbnailFromUrl(videoInstance, videoObject.icon)
+ .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err }))
// Remove old video files
const videoFileDestroyTasks: Bluebird[] = []
@@ -108,16 +113,16 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
}
await Promise.all(videoFileDestroyTasks)
- const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoInstance, videoAttributesToUpdate)
+ const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoInstance, videoObject)
const tasks = videoFileAttributes.map(f => VideoFileModel.create(f))
await Promise.all(tasks)
- const tags = videoAttributesToUpdate.tag.map(t => t.name)
+ const tags = videoObject.tag.map(t => t.name)
const tagInstances = await TagModel.findOrCreateTags(tags, t)
await videoInstance.$set('Tags', tagInstances, sequelizeOptions)
})
- logger.info('Remote video with uuid %s updated', videoAttributesToUpdate.uuid)
+ logger.info('Remote video with uuid %s updated', videoObject.uuid)
} catch (err) {
if (videoInstance !== undefined && videoFieldsSave !== undefined) {
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
const accountsToForwardView = await getActorsInvolvedInVideo(video, t)
const audience = getObjectFollowersAudience(accountsToForwardView)
- return announceActivityData(videoShare.url, byActor, announcedObject, t, audience)
+ return announceActivityData(videoShare.url, byActor, announcedObject, audience)
}
async function sendVideoAnnounce (byActor: ActorModel, videoShare: VideoShareModel, video: VideoModel, t: Transaction) {
@@ -20,16 +20,8 @@ async function sendVideoAnnounce (byActor: ActorModel, videoShare: VideoShareMod
return broadcastToFollowers(data, byActor, [ byActor ], t)
}
-async function announceActivityData (
- url: string,
- byActor: ActorModel,
- object: string,
- t: Transaction,
- audience?: ActivityAudience
-): Promise {
- if (!audience) {
- audience = await getAudience(byActor, t)
- }
+function announceActivityData (url: string, byActor: ActorModel, object: string, audience?: ActivityAudience): ActivityAnnounce {
+ if (!audience) audience = getAudience(byActor)
return {
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) {
const byActor = video.VideoChannel.Account.Actor
const videoObject = video.toActivityPubObject()
- const audience = await getAudience(byActor, t, video.privacy === VideoPrivacy.PUBLIC)
- const data = await createActivityData(video.url, byActor, videoObject, t, audience)
+ const audience = getAudience(byActor, video.privacy === VideoPrivacy.PUBLIC)
+ const data = createActivityData(video.url, byActor, videoObject, audience)
return broadcastToFollowers(data, byActor, [ byActor ], t)
}
@@ -33,7 +33,7 @@ async function sendVideoAbuse (byActor: ActorModel, videoAbuse: VideoAbuseModel,
const url = getVideoAbuseActivityPubUrl(videoAbuse)
const audience = { to: [ video.VideoChannel.Account.Actor.url ], cc: [] }
- const data = await createActivityData(url, byActor, videoAbuse.toActivityPubObject(), t, audience)
+ const data = createActivityData(url, byActor, videoAbuse.toActivityPubObject(), audience)
return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
}
@@ -57,7 +57,7 @@ async function sendCreateVideoComment (comment: VideoCommentModel, t: Transactio
audience = getObjectFollowersAudience(actorsInvolvedInComment.concat(parentsCommentActors))
}
- const data = await createActivityData(comment.url, byActor, commentObject, t, audience)
+ const data = createActivityData(comment.url, byActor, commentObject, audience)
// This was a reply, send it to the parent actors
const actorsException = [ byActor ]
@@ -82,14 +82,14 @@ async function sendCreateView (byActor: ActorModel, video: VideoModel, t: Transa
// Send to origin
if (video.isOwned() === false) {
const audience = getVideoAudience(video, actorsInvolvedInVideo)
- const data = await createActivityData(url, byActor, viewActivityData, t, audience)
+ const data = createActivityData(url, byActor, viewActivityData, audience)
return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
}
// Send to followers
const audience = getObjectFollowersAudience(actorsInvolvedInVideo)
- const data = await createActivityData(url, byActor, viewActivityData, t, audience)
+ const data = createActivityData(url, byActor, viewActivityData, audience)
// Use the server actor to send the view
const serverActor = await getServerActor()
@@ -106,34 +106,31 @@ async function sendCreateDislike (byActor: ActorModel, video: VideoModel, t: Tra
// Send to origin
if (video.isOwned() === false) {
const audience = getVideoAudience(video, actorsInvolvedInVideo)
- const data = await createActivityData(url, byActor, dislikeActivityData, t, audience)
+ const data = createActivityData(url, byActor, dislikeActivityData, audience)
return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
}
// Send to followers
const audience = getObjectFollowersAudience(actorsInvolvedInVideo)
- const data = await createActivityData(url, byActor, dislikeActivityData, t, audience)
+ const data = createActivityData(url, byActor, dislikeActivityData, audience)
const actorsException = [ byActor ]
return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, actorsException)
}
-async function createActivityData (url: string,
- byActor: ActorModel,
- object: any,
- t: Transaction,
- audience?: ActivityAudience): Promise {
- if (!audience) {
- audience = await getAudience(byActor, t)
- }
+function createActivityData (url: string, byActor: ActorModel, object: any, audience?: ActivityAudience): ActivityCreate {
+ if (!audience) audience = getAudience(byActor)
- return audiencify({
- type: 'Create' as 'Create',
- id: url + '/activity',
- actor: byActor.url,
- object: audiencify(object, audience)
- }, audience)
+ return audiencify(
+ {
+ type: 'Create' as 'Create',
+ id: url + '/activity',
+ actor: byActor.url,
+ object: audiencify(object, audience)
+ },
+ audience
+ )
}
function 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)
// Send to origin
if (video.isOwned() === false) {
const audience = getVideoAudience(video, accountsInvolvedInVideo)
- const data = await likeActivityData(url, byActor, video, t, audience)
+ const data = likeActivityData(url, byActor, video, audience)
return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
}
// Send to followers
const audience = getObjectFollowersAudience(accountsInvolvedInVideo)
- const data = await likeActivityData(url, byActor, video, t, audience)
+ const data = likeActivityData(url, byActor, video, audience)
const followersException = [ byActor ]
return broadcastToFollowers(data, byActor, accountsInvolvedInVideo, t, followersException)
}
-async function likeActivityData (
- url: string,
- byActor: ActorModel,
- video: VideoModel,
- t: Transaction,
- audience?: ActivityAudience
-): Promise {
- if (!audience) {
- audience = await getAudience(byActor, t)
- }
+function likeActivityData (url: string, byActor: ActorModel, video: VideoModel, audience?: ActivityAudience): ActivityLike {
+ if (!audience) audience = getAudience(byActor)
- return audiencify({
- type: 'Like' as 'Like',
- id: url,
- actor: byActor.url,
- object: video.url
- }, audience)
+ return audiencify(
+ {
+ type: 'Like' as 'Like',
+ id: url,
+ actor: byActor.url,
+ object: video.url
+ },
+ audience
+ )
}
// ---------------------------------------------------------------------------
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) {
const undoUrl = getUndoActivityPubUrl(followUrl)
const object = followActivityData(followUrl, me, following)
- const data = await undoActivityData(undoUrl, me, object, t)
+ const data = undoActivityData(undoUrl, me, object)
return unicastTo(data, me, following.inboxUrl)
}
@@ -37,18 +37,18 @@ async function sendUndoLike (byActor: ActorModel, video: VideoModel, t: Transact
const undoUrl = getUndoActivityPubUrl(likeUrl)
const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
- const object = await likeActivityData(likeUrl, byActor, video, t)
+ const object = likeActivityData(likeUrl, byActor, video)
// Send to origin
if (video.isOwned() === false) {
const audience = getVideoAudience(video, actorsInvolvedInVideo)
- const data = await undoActivityData(undoUrl, byActor, object, t, audience)
+ const data = undoActivityData(undoUrl, byActor, object, audience)
return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
}
const audience = getObjectFollowersAudience(actorsInvolvedInVideo)
- const data = await undoActivityData(undoUrl, byActor, object, t, audience)
+ const data = undoActivityData(undoUrl, byActor, object, audience)
const followersException = [ byActor ]
return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException)
@@ -60,16 +60,16 @@ async function sendUndoDislike (byActor: ActorModel, video: VideoModel, t: Trans
const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
const dislikeActivity = createDislikeActivityData(byActor, video)
- const object = await createActivityData(dislikeUrl, byActor, dislikeActivity, t)
+ const object = createActivityData(dislikeUrl, byActor, dislikeActivity)
if (video.isOwned() === false) {
const audience = getVideoAudience(video, actorsInvolvedInVideo)
- const data = await undoActivityData(undoUrl, byActor, object, t, audience)
+ const data = undoActivityData(undoUrl, byActor, object, audience)
return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
}
- const data = await undoActivityData(undoUrl, byActor, object, t)
+ const data = undoActivityData(undoUrl, byActor, object)
const followersException = [ byActor ]
return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException)
@@ -80,7 +80,7 @@ async function sendUndoAnnounce (byActor: ActorModel, videoShare: VideoShareMode
const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
const object = await buildVideoAnnounce(byActor, videoShare, video, t)
- const data = await undoActivityData(undoUrl, byActor, object, t)
+ const data = undoActivityData(undoUrl, byActor, object)
const followersException = [ byActor ]
return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException)
@@ -97,21 +97,21 @@ export {
// ---------------------------------------------------------------------------
-async function undoActivityData (
+function undoActivityData (
url: string,
byActor: ActorModel,
object: ActivityFollow | ActivityLike | ActivityCreate | ActivityAnnounce,
- t: Transaction,
audience?: ActivityAudience
-): Promise {
- if (!audience) {
- audience = await getAudience(byActor, t)
- }
+): ActivityUndo {
+ if (!audience) audience = getAudience(byActor)
- return audiencify({
- type: 'Undo' as 'Undo',
- id: url,
- actor: byActor.url,
- object
- }, audience)
+ return audiencify(
+ {
+ type: 'Undo' as 'Undo',
+ id: url,
+ actor: byActor.url,
+ object
+ },
+ audience
+ )
}
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) {
const url = getUpdateActivityPubUrl(video.url, video.updatedAt.toISOString())
const videoObject = video.toActivityPubObject()
- const audience = await getAudience(byActor, t, video.privacy === VideoPrivacy.PUBLIC)
+ const audience = getAudience(byActor, video.privacy === VideoPrivacy.PUBLIC)
- const data = await updateActivityData(url, byActor, videoObject, t, audience)
+ const data = updateActivityData(url, byActor, videoObject, audience)
const actorsInvolved = await VideoShareModel.loadActorsByShare(video.id, t)
actorsInvolved.push(byActor)
@@ -30,8 +30,8 @@ async function sendUpdateActor (accountOrChannel: AccountModel | VideoChannelMod
const url = getUpdateActivityPubUrl(byActor.url, byActor.updatedAt.toISOString())
const accountOrChannelObject = accountOrChannel.toActivityPubObject()
- const audience = await getAudience(byActor, t)
- const data = await updateActivityData(url, byActor, accountOrChannelObject, t, audience)
+ const audience = getAudience(byActor)
+ const data = updateActivityData(url, byActor, accountOrChannelObject, audience)
let actorsInvolved: ActorModel[]
if (accountOrChannel instanceof AccountModel) {
@@ -56,21 +56,17 @@ export {
// ---------------------------------------------------------------------------
-async function updateActivityData (
- url: string,
- byActor: ActorModel,
- object: any,
- t: Transaction,
- audience?: ActivityAudience
-): Promise {
- if (!audience) {
- audience = await getAudience(byActor, t)
- }
+function updateActivityData (url: string, byActor: ActorModel, object: any, audience?: ActivityAudience): ActivityUpdate {
+ if (!audience) audience = getAudience(byActor)
- return audiencify({
- type: 'Update' as 'Update',
- id: url,
- actor: byActor.url,
- object: audiencify(object, audience)
- }, audience)
+ return audiencify(
+ {
+ type: 'Update' as 'Update',
+ id: url,
+ actor: byActor.url,
+ object: audiencify(object, audience
+ )
+ },
+ audience
+ )
}
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 @@
import * as Bluebird from 'bluebird'
+import * as sequelize from 'sequelize'
import * as magnetUtil from 'magnet-uri'
import { join } from 'path'
import * as request from 'request'
-import { ActivityIconObject } from '../../../shared/index'
+import { ActivityIconObject, VideoState } from '../../../shared/index'
import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos'
import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos'
@@ -21,6 +22,21 @@ import { VideoShareModel } from '../../models/video/video-share'
import { getOrCreateActorAndServerAndModel } from './actor'
import { addVideoComments } from './video-comments'
import { crawlCollectionPage } from './crawl'
+import { sendCreateVideo, sendUpdateVideo } from './send'
+import { shareVideoByServerAndChannel } from './index'
+
+async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
+ // If the video is not private and published, we federate it
+ if (video.privacy !== VideoPrivacy.PRIVATE && video.state === VideoState.PUBLISHED) {
+ if (isNewVideo === true) {
+ // Now we'll add the video's meta data to our followers
+ await sendCreateVideo(video, transaction)
+ await shareVideoByServerAndChannel(video, transaction)
+ } else {
+ await sendUpdateVideo(video, transaction)
+ }
+ }
+}
function fetchRemoteVideoPreview (video: VideoModel, reject: Function) {
const host = video.VideoChannel.Account.Actor.Server.host
@@ -55,9 +71,11 @@ function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject)
return doRequestAndSaveToFile(options, thumbnailPath)
}
-async function videoActivityObjectToDBAttributes (videoChannel: VideoChannelModel,
- videoObject: VideoTorrentObject,
- to: string[] = []) {
+async function videoActivityObjectToDBAttributes (
+ videoChannel: VideoChannelModel,
+ videoObject: VideoTorrentObject,
+ to: string[] = []
+) {
const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPrivacy.PUBLIC : VideoPrivacy.UNLISTED
const duration = videoObject.duration.replace(/[^\d]+/, '')
@@ -90,6 +108,8 @@ async function videoActivityObjectToDBAttributes (videoChannel: VideoChannelMode
support,
nsfw: videoObject.sensitive,
commentsEnabled: videoObject.commentsEnabled,
+ waitTranscoding: videoObject.waitTranscoding,
+ state: videoObject.state,
channelId: videoChannel.id,
duration: parseInt(duration, 10),
createdAt: new Date(videoObject.published),
@@ -185,22 +205,20 @@ async function getOrCreateVideo (videoObject: VideoTorrentObject, channelActor:
}
async function getOrCreateAccountAndVideoAndChannel (videoObject: VideoTorrentObject | string, actor?: ActorModel) {
- if (typeof videoObject === 'string') {
- const videoUrl = videoObject
+ const videoUrl = typeof videoObject === 'string' ? videoObject : videoObject.id
- const videoFromDatabase = await VideoModel.loadByUrlAndPopulateAccount(videoUrl)
- if (videoFromDatabase) {
- return {
- video: videoFromDatabase,
- actor: videoFromDatabase.VideoChannel.Account.Actor,
- channelActor: videoFromDatabase.VideoChannel.Actor
- }
+ const videoFromDatabase = await VideoModel.loadByUrlAndPopulateAccount(videoUrl)
+ if (videoFromDatabase) {
+ return {
+ video: videoFromDatabase,
+ actor: videoFromDatabase.VideoChannel.Account.Actor,
+ channelActor: videoFromDatabase.VideoChannel.Actor
}
-
- videoObject = await fetchRemoteVideo(videoUrl)
- if (!videoObject) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
}
+ videoObject = await fetchRemoteVideo(videoUrl)
+ if (!videoObject) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
+
if (!actor) {
const actorObj = videoObject.attributedTo.find(a => a.type === 'Person')
if (!actorObj) throw new Error('Cannot find associated actor to video ' + videoObject.url)
@@ -291,20 +309,6 @@ async function addVideoShares (shareUrls: string[], instance: VideoModel) {
}
}
-export {
- getOrCreateAccountAndVideoAndChannel,
- fetchRemoteVideoPreview,
- fetchRemoteVideoDescription,
- generateThumbnailFromUrl,
- videoActivityObjectToDBAttributes,
- videoFileActivityUrlToDBAttributes,
- getOrCreateVideo,
- getOrCreateVideoChannel,
- addVideoShares
-}
-
-// ---------------------------------------------------------------------------
-
async function fetchRemoteVideo (videoUrl: string): Promise {
const options = {
uri: videoUrl,
@@ -324,3 +328,17 @@ async function fetchRemoteVideo (videoUrl: string): Promise
return body
}
+
+export {
+ federateVideoIfNeeded,
+ fetchRemoteVideo,
+ getOrCreateAccountAndVideoAndChannel,
+ fetchRemoteVideoPreview,
+ fetchRemoteVideoDescription,
+ generateThumbnailFromUrl,
+ videoActivityObjectToDBAttributes,
+ videoFileActivityUrlToDBAttributes,
+ getOrCreateVideo,
+ getOrCreateVideoChannel,
+ addVideoShares
+}
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 @@
import * as kue from 'kue'
-import { VideoResolution } from '../../../../shared'
-import { VideoPrivacy } from '../../../../shared/models/videos'
+import { VideoResolution, VideoState } from '../../../../shared'
import { logger } from '../../../helpers/logger'
import { computeResolutionsToTranscode } from '../../../helpers/utils'
-import { sequelizeTypescript } from '../../../initializers'
import { VideoModel } from '../../../models/video/video'
-import { shareVideoByServerAndChannel } from '../../activitypub'
-import { sendCreateVideo, sendUpdateVideo } from '../../activitypub/send'
import { JobQueue } from '../job-queue'
+import { federateVideoIfNeeded } from '../../activitypub'
+import { retryTransactionWrapper } from '../../../helpers/database-utils'
+import { sequelizeTypescript } from '../../../initializers'
export type VideoFilePayload = {
videoUUID: string
- isNewVideo: boolean
+ isNewVideo?: boolean
resolution?: VideoResolution
isPortraitMode?: boolean
}
@@ -52,10 +51,20 @@ async function processVideoFile (job: kue.Job) {
// Transcoding in other resolution
if (payload.resolution) {
await video.transcodeOriginalVideofile(payload.resolution, payload.isPortraitMode)
- await onVideoFileTranscoderOrImportSuccess(video)
+
+ const options = {
+ arguments: [ video ],
+ errorMessage: 'Cannot execute onVideoFileTranscoderOrImportSuccess with many retries.'
+ }
+ await retryTransactionWrapper(onVideoFileTranscoderOrImportSuccess, options)
} else {
await video.optimizeOriginalVideofile()
- await onVideoFileOptimizerSuccess(video, payload.isNewVideo)
+
+ const options = {
+ arguments: [ video, payload.isNewVideo ],
+ errorMessage: 'Cannot execute onVideoFileOptimizerSuccess with many retries.'
+ }
+ await retryTransactionWrapper(onVideoFileOptimizerSuccess, options)
}
return video
@@ -64,68 +73,70 @@ async function processVideoFile (job: kue.Job) {
async function onVideoFileTranscoderOrImportSuccess (video: VideoModel) {
if (video === undefined) return undefined
- // Maybe the video changed in database, refresh it
- const videoDatabase = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(video.uuid)
- // Video does not exist anymore
- if (!videoDatabase) return undefined
+ return sequelizeTypescript.transaction(async t => {
+ // Maybe the video changed in database, refresh it
+ let videoDatabase = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(video.uuid, t)
+ // Video does not exist anymore
+ if (!videoDatabase) return undefined
- if (video.privacy !== VideoPrivacy.PRIVATE) {
- await sendUpdateVideo(video, undefined)
- }
+ // We transcoded the video file in another format, now we can publish it
+ const oldState = videoDatabase.state
+ videoDatabase.state = VideoState.PUBLISHED
+ videoDatabase = await videoDatabase.save({ transaction: t })
- return undefined
+ // If the video was not published, we consider it is a new one for other instances
+ const isNewVideo = oldState !== VideoState.PUBLISHED
+ await federateVideoIfNeeded(videoDatabase, isNewVideo, t)
+
+ return undefined
+ })
}
async function onVideoFileOptimizerSuccess (video: VideoModel, isNewVideo: boolean) {
if (video === undefined) return undefined
- // Maybe the video changed in database, refresh it
- const videoDatabase = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(video.uuid)
- // Video does not exist anymore
- if (!videoDatabase) return undefined
+ // Outside the transaction (IO on disk)
+ const { videoFileResolution } = await video.getOriginalFileResolution()
- if (video.privacy !== VideoPrivacy.PRIVATE) {
- if (isNewVideo !== false) {
- // Now we'll add the video's meta data to our followers
- await sequelizeTypescript.transaction(async t => {
- await sendCreateVideo(video, t)
- await shareVideoByServerAndChannel(video, t)
- })
- } else {
- await sendUpdateVideo(video, undefined)
- }
- }
+ return sequelizeTypescript.transaction(async t => {
+ // Maybe the video changed in database, refresh it
+ const videoDatabase = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(video.uuid, t)
+ // Video does not exist anymore
+ if (!videoDatabase) return undefined
- const { videoFileResolution } = await videoDatabase.getOriginalFileResolution()
+ // Create transcoding jobs if there are enabled resolutions
+ const resolutionsEnabled = computeResolutionsToTranscode(videoFileResolution)
+ logger.info(
+ 'Resolutions computed for video %s and origin file height of %d.', videoDatabase.uuid, videoFileResolution,
+ { resolutions: resolutionsEnabled }
+ )
- // Create transcoding jobs if there are enabled resolutions
- const resolutionsEnabled = computeResolutionsToTranscode(videoFileResolution)
- logger.info(
- 'Resolutions computed for video %s and origin file height of %d.', videoDatabase.uuid, videoFileResolution,
- { resolutions: resolutionsEnabled }
- )
+ if (resolutionsEnabled.length !== 0) {
+ const tasks: Promise[] = []
- if (resolutionsEnabled.length !== 0) {
- const tasks: Promise[] = []
+ for (const resolution of resolutionsEnabled) {
+ const dataInput = {
+ videoUUID: videoDatabase.uuid,
+ resolution
+ }
- for (const resolution of resolutionsEnabled) {
- const dataInput = {
- videoUUID: videoDatabase.uuid,
- resolution,
- isNewVideo
+ const p = JobQueue.Instance.createJob({ type: 'video-file', payload: dataInput })
+ tasks.push(p)
}
- const p = JobQueue.Instance.createJob({ type: 'video-file', payload: dataInput })
- tasks.push(p)
+ await Promise.all(tasks)
+
+ logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled })
+ } else {
+ // No transcoding to do, it's now published
+ video.state = VideoState.PUBLISHED
+ video = await video.save({ transaction: t })
+
+ logger.info('No transcoding jobs created for video %s (no resolutions).', video.uuid)
}
- await Promise.all(tasks)
-
- logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled })
- } else {
- logger.info('No transcoding jobs created for video %s (no resolutions enabled).')
- return undefined
- }
+ return federateVideoIfNeeded(video, isNewVideo, t)
+ })
}
// ---------------------------------------------------------------------------
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 {
const res = await handlers[ handlerName ](job)
return done(null, res)
} catch (err) {
+ logger.error('Cannot execute job %d.', job.id, { err })
return done(err)
}
})
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) {
// Not cached
if (!cached) {
- logger.debug('Not cached result for route %s.', req.originalUrl)
+ logger.debug('No cached results for route %s.', req.originalUrl)
const sendSave = res.send.bind(res)
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 = [
.customSanitizer(toValueOrNull)
.custom(isVideoLanguageValid).withMessage('Should have a valid language'),
body('nsfw')
+ .optional()
.toBoolean()
.custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
+ body('waitTranscoding')
+ .optional()
+ .toBoolean()
+ .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
body('description')
.optional()
.customSanitizer(toValueOrNull)
@@ -70,6 +75,7 @@ const videosAddValidator = [
.customSanitizer(toValueOrNull)
.custom(isVideoTagsValid).withMessage('Should have correct tags'),
body('commentsEnabled')
+ .optional()
.toBoolean()
.custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
body('privacy')
@@ -149,6 +155,10 @@ const videosUpdateValidator = [
.optional()
.toBoolean()
.custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
+ body('waitTranscoding')
+ .optional()
+ .toBoolean()
+ .custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
body('privacy')
.optional()
.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 {
Table,
UpdatedAt
} from 'sequelize-typescript'
-import { VideoPrivacy, VideoResolution } from '../../../shared'
+import { VideoPrivacy, VideoResolution, VideoState } from '../../../shared'
import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
import { VideoFilter } from '../../../shared/models/videos/video-query.type'
@@ -47,7 +47,7 @@ import {
isVideoLanguageValid,
isVideoLicenceValid,
isVideoNameValid,
- isVideoPrivacyValid,
+ isVideoPrivacyValid, isVideoStateValid,
isVideoSupportValid
} from '../../helpers/custom-validators/videos'
import { generateImageFromVideoFile, getVideoFileResolution, transcode } from '../../helpers/ffmpeg-utils'
@@ -66,7 +66,7 @@ import {
VIDEO_EXT_MIMETYPE,
VIDEO_LANGUAGES,
VIDEO_LICENCES,
- VIDEO_PRIVACIES
+ VIDEO_PRIVACIES, VIDEO_STATES
} from '../../initializers'
import {
getVideoCommentsActivityPubUrl,
@@ -93,10 +93,7 @@ enum ScopeNames {
AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS',
WITH_TAGS = 'WITH_TAGS',
- WITH_FILES = 'WITH_FILES',
- WITH_SHARES = 'WITH_SHARES',
- WITH_RATES = 'WITH_RATES',
- WITH_COMMENTS = 'WITH_COMMENTS'
+ WITH_FILES = 'WITH_FILES'
}
@Scopes({
@@ -183,7 +180,20 @@ enum ScopeNames {
')'
)
},
- privacy: VideoPrivacy.PUBLIC
+ // Always list public videos
+ privacy: VideoPrivacy.PUBLIC,
+ // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding
+ [ Sequelize.Op.or ]: [
+ {
+ state: VideoState.PUBLISHED
+ },
+ {
+ [ Sequelize.Op.and ]: {
+ state: VideoState.TO_TRANSCODE,
+ waitTranscoding: false
+ }
+ }
+ ]
},
include: [ videoChannelInclude ]
}
@@ -272,42 +282,6 @@ enum ScopeNames {
required: true
}
]
- },
- [ScopeNames.WITH_SHARES]: {
- include: [
- {
- ['separate' as any]: true,
- model: () => VideoShareModel.unscoped()
- }
- ]
- },
- [ScopeNames.WITH_RATES]: {
- include: [
- {
- ['separate' as any]: true,
- model: () => AccountVideoRateModel,
- include: [
- {
- model: () => AccountModel.unscoped(),
- required: true,
- include: [
- {
- attributes: [ 'url' ],
- model: () => ActorModel.unscoped()
- }
- ]
- }
- ]
- }
- ]
- },
- [ScopeNames.WITH_COMMENTS]: {
- include: [
- {
- ['separate' as any]: true,
- model: () => VideoCommentModel.unscoped()
- }
- ]
}
})
@Table({
@@ -335,7 +309,7 @@ enum ScopeNames {
fields: [ 'channelId' ]
},
{
- fields: [ 'id', 'privacy' ]
+ fields: [ 'id', 'privacy', 'state', 'waitTranscoding' ]
},
{
fields: [ 'url'],
@@ -435,6 +409,16 @@ export class VideoModel extends Model {
@Column
commentsEnabled: boolean
+ @AllowNull(false)
+ @Column
+ waitTranscoding: boolean
+
+ @AllowNull(false)
+ @Default(null)
+ @Is('VideoState', value => throwIfNotValid(value, isVideoStateValid, 'state'))
+ @Column
+ state: VideoState
+
@CreatedAt
createdAt: Date
@@ -671,7 +655,7 @@ export class VideoModel extends Model {
})
}
- static listAccountVideosForApi (accountId: number, start: number, count: number, sort: string, hideNSFW: boolean, withFiles = false) {
+ static listUserVideosForApi (accountId: number, start: number, count: number, sort: string, hideNSFW: boolean, withFiles = false) {
const query: IFindOptions = {
offset: start,
limit: count,
@@ -858,12 +842,13 @@ export class VideoModel extends Model {
.findOne(options)
}
- static loadByUUIDAndPopulateAccountAndServerAndTags (uuid: string) {
+ static loadByUUIDAndPopulateAccountAndServerAndTags (uuid: string, t?: Sequelize.Transaction) {
const options = {
order: [ [ 'Tags', 'name', 'ASC' ] ],
where: {
uuid
- }
+ },
+ transaction: t
}
return VideoModel
@@ -905,31 +890,23 @@ export class VideoModel extends Model {
}
private static getCategoryLabel (id: number) {
- let categoryLabel = VIDEO_CATEGORIES[id]
- if (!categoryLabel) categoryLabel = 'Misc'
-
- return categoryLabel
+ return VIDEO_CATEGORIES[id] || 'Misc'
}
private static getLicenceLabel (id: number) {
- let licenceLabel = VIDEO_LICENCES[id]
- if (!licenceLabel) licenceLabel = 'Unknown'
-
- return licenceLabel
+ return VIDEO_LICENCES[id] || 'Unknown'
}
private static getLanguageLabel (id: string) {
- let languageLabel = VIDEO_LANGUAGES[id]
- if (!languageLabel) languageLabel = 'Unknown'
-
- return languageLabel
+ return VIDEO_LANGUAGES[id] || 'Unknown'
}
private static getPrivacyLabel (id: number) {
- let privacyLabel = VIDEO_PRIVACIES[id]
- if (!privacyLabel) privacyLabel = 'Unknown'
+ return VIDEO_PRIVACIES[id] || 'Unknown'
+ }
- return privacyLabel
+ private static getStateLabel (id: number) {
+ return VIDEO_STATES[id] || 'Unknown'
}
getOriginalFile () {
@@ -1026,11 +1003,16 @@ export class VideoModel extends Model {
return join(STATIC_PATHS.PREVIEWS, this.getPreviewName())
}
- toFormattedJSON (): Video {
+ toFormattedJSON (options?: {
+ additionalAttributes: {
+ state: boolean,
+ waitTranscoding: boolean
+ }
+ }): Video {
const formattedAccount = this.VideoChannel.Account.toFormattedJSON()
const formattedVideoChannel = this.VideoChannel.toFormattedJSON()
- return {
+ const videoObject: Video = {
id: this.id,
uuid: this.uuid,
name: this.name,
@@ -1082,6 +1064,19 @@ export class VideoModel extends Model {
avatar: formattedVideoChannel.avatar
}
}
+
+ if (options) {
+ if (options.additionalAttributes.state) {
+ videoObject.state = {
+ id: this.state,
+ label: VideoModel.getStateLabel(this.state)
+ }
+ }
+
+ if (options.additionalAttributes.waitTranscoding) videoObject.waitTranscoding = this.waitTranscoding
+ }
+
+ return videoObject
}
toFormattedDetailsJSON (): VideoDetails {
@@ -1094,6 +1089,11 @@ export class VideoModel extends Model {
account: this.VideoChannel.Account.toFormattedJSON(),
tags: map(this.Tags, 'name'),
commentsEnabled: this.commentsEnabled,
+ waitTranscoding: this.waitTranscoding,
+ state: {
+ id: this.state,
+ label: VideoModel.getStateLabel(this.state)
+ },
files: []
}
@@ -1207,6 +1207,8 @@ export class VideoModel extends Model {
language,
views: this.views,
sensitive: this.nsfw,
+ waitTranscoding: this.waitTranscoding,
+ state: this.state,
commentsEnabled: this.commentsEnabled,
published: this.publishedAt.toISOString(),
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 () {
language: 'pt',
nsfw: false,
commentsEnabled: true,
+ waitTranscoding: true,
description: 'my super description',
support: 'my super support text',
tags: [ 'tag1', 'tag2' ],
@@ -224,20 +225,6 @@ describe('Test videos API validator', function () {
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
})
- it('Should fail without nsfw attribute', async function () {
- const fields = omit(baseCorrectParams, 'nsfw')
- const attaches = baseCorrectAttaches
-
- await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
- })
-
- it('Should fail without commentsEnabled attribute', async function () {
- const fields = omit(baseCorrectParams, 'commentsEnabled')
- const attaches = baseCorrectAttaches
-
- await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
- })
-
it('Should fail with a long description', async function () {
const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(2500) })
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 () {
describe('With minimum parameters', function () {
it('Should upload and propagate the video', async function () {
- this.timeout(50000)
+ this.timeout(60000)
const path = '/api/v1/videos/upload'
@@ -934,16 +934,14 @@ describe('Test multiple servers', function () {
.set('Authorization', 'Bearer ' + servers[1].accessToken)
.field('name', 'minimum parameters')
.field('privacy', '1')
- .field('nsfw', 'false')
.field('channelId', '1')
- .field('commentsEnabled', 'true')
const filePath = join(__dirname, '..', '..', 'fixtures', 'video_short.webm')
await req.attach('videofile', filePath)
.expect(200)
- await wait(25000)
+ await wait(40000)
for (const server of servers) {
const res = await getVideosList(server.url)
@@ -964,7 +962,7 @@ describe('Test multiple servers', function () {
},
isLocal,
duration: 5,
- commentsEnabled: true,
+ commentsEnabled: false,
tags: [ ],
privacy: VideoPrivacy.PUBLIC,
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 () {
const oembedUrl = 'http://localhost:9001/videos/watch/' + server.video.uuid
const res = await getOEmbed(server.url, oembedUrl)
- const expectedHtml = `'
const expectedThumbnailUrl = 'http://localhost:9001/static/previews/' + server.video.uuid + '.jpg'
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 @@
import * as chai from 'chai'
import 'mocha'
-import { VideoDetails } from '../../../../shared/models/videos'
+import { VideoDetails, VideoState } from '../../../../shared/models/videos'
import { getVideoFileFPS } from '../../../helpers/ffmpeg-utils'
import {
- flushAndRunMultipleServers, flushTests, getVideo, getVideosList, killallServers, root, ServerInfo, setAccessTokensToServers, uploadVideo,
- wait, webtorrentAdd
+ doubleFollow,
+ flushAndRunMultipleServers,
+ flushTests,
+ getMyVideos,
+ getVideo,
+ getVideosList,
+ killallServers,
+ root,
+ ServerInfo,
+ setAccessTokensToServers,
+ uploadVideo,
+ wait,
+ webtorrentAdd
} from '../../utils'
import { join } from 'path'
@@ -109,6 +120,63 @@ describe('Test video transcoding', function () {
}
})
+ it('Should wait transcoding before publishing the video', async function () {
+ this.timeout(80000)
+
+ await doubleFollow(servers[0], servers[1])
+
+ await wait(15000)
+
+ {
+ // Upload the video, but wait transcoding
+ const videoAttributes = {
+ name: 'waiting video',
+ fixture: 'video_short1.webm',
+ waitTranscoding: true
+ }
+ const resVideo = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, videoAttributes)
+ const videoId = resVideo.body.video.uuid
+
+ // Should be in transcode state
+ const { body } = await getVideo(servers[ 1 ].url, videoId)
+ expect(body.name).to.equal('waiting video')
+ expect(body.state.id).to.equal(VideoState.TO_TRANSCODE)
+ expect(body.state.label).to.equal('To transcode')
+ expect(body.waitTranscoding).to.be.true
+
+ // Should have my video
+ const resMyVideos = await getMyVideos(servers[1].url, servers[1].accessToken, 0, 10)
+ const videoToFindInMine = resMyVideos.body.data.find(v => v.name === 'waiting video')
+ expect(videoToFindInMine).not.to.be.undefined
+ expect(videoToFindInMine.state.id).to.equal(VideoState.TO_TRANSCODE)
+ expect(videoToFindInMine.state.label).to.equal('To transcode')
+ expect(videoToFindInMine.waitTranscoding).to.be.true
+
+ // Should not list this video
+ const resVideos = await getVideosList(servers[1].url)
+ const videoToFindInList = resVideos.body.data.find(v => v.name === 'waiting video')
+ expect(videoToFindInList).to.be.undefined
+
+ // Server 1 should not have the video yet
+ await getVideo(servers[0].url, videoId, 404)
+ }
+
+ await wait(30000)
+
+ for (const server of servers) {
+ const res = await getVideosList(server.url)
+ const videoToFind = res.body.data.find(v => v.name === 'waiting video')
+ expect(videoToFind).not.to.be.undefined
+
+ const res2 = await getVideo(server.url, videoToFind.id)
+ const videoDetails: VideoDetails = res2.body
+
+ expect(videoDetails.state.id).to.equal(VideoState.PUBLISHED)
+ expect(videoDetails.state.label).to.equal('Published')
+ expect(videoDetails.waitTranscoding).to.be.true
+ }
+ })
+
after(async function () {
killallServers(servers)
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 () {
const env = getEnvCli(servers[0])
await execCLI(`${env} npm run create-transcoding-job -- -v ${video2UUID}`)
- await wait(30000)
+ await wait(40000)
for (const server of servers) {
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 = {
language?: string
nsfw?: boolean
commentsEnabled?: boolean
+ waitTranscoding?: boolean
description?: string
tags?: string[]
channelId?: number
@@ -326,6 +327,7 @@ async function uploadVideo (url: string, accessToken: string, videoAttributesArg
language: 'zh',
channelId: defaultChannelId,
nsfw: true,
+ waitTranscoding: false,
description: 'my super description',
support: 'my super support text',
tags: [ 'tag' ],
@@ -341,6 +343,7 @@ async function uploadVideo (url: string, accessToken: string, videoAttributesArg
.field('name', attributes.name)
.field('nsfw', JSON.stringify(attributes.nsfw))
.field('commentsEnabled', JSON.stringify(attributes.commentsEnabled))
+ .field('waitTranscoding', JSON.stringify(attributes.waitTranscoding))
.field('privacy', attributes.privacy.toString())
.field('channelId', attributes.channelId)
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
licence,
language,
nsfw: isNSFW(videoInfo),
+ waitTranscoding: true,
commentsEnabled: true,
description: videoInfo.description || undefined,
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 () {
fixture: program['file'],
thumbnailfile: program['thumbnailPath'],
previewfile: program['previewPath'],
+ waitTranscoding: true,
privacy: program['privacy'],
support: undefined
}
diff --git a/shared/models/activitypub/objects/video-torrent-object.ts b/shared/models/activitypub/objects/video-torrent-object.ts
index 767b6a2d0..c4071a6d9 100644
--- a/shared/models/activitypub/objects/video-torrent-object.ts
+++ b/shared/models/activitypub/objects/video-torrent-object.ts
@@ -5,6 +5,7 @@ import {
ActivityUrlObject
} from './common-objects'
import { ActivityPubOrderedCollection } from '../activitypub-ordered-collection'
+import { VideoState } from '../../videos'
export interface VideoTorrentObject {
type: 'Video'
@@ -19,6 +20,8 @@ export interface VideoTorrentObject {
views: number
sensitive: boolean
commentsEnabled: boolean
+ waitTranscoding: boolean
+ state: VideoState
published: string
updated: string
mediaType: 'text/markdown'
diff --git a/shared/models/videos/index.ts b/shared/models/videos/index.ts
index 14a10f5d8..9edfb559a 100644
--- a/shared/models/videos/index.ts
+++ b/shared/models/videos/index.ts
@@ -13,3 +13,4 @@ export * from './video-rate.type'
export * from './video-resolution.enum'
export * from './video-update.model'
export * from './video.model'
+export * from './video-state.enum'
diff --git a/shared/models/videos/video-create.model.ts b/shared/models/videos/video-create.model.ts
index 562bc1bf2..2a1f622f6 100644
--- a/shared/models/videos/video-create.model.ts
+++ b/shared/models/videos/video-create.model.ts
@@ -7,7 +7,8 @@ export interface VideoCreate {
description?: string
support?: string
channelId: number
- nsfw: boolean
+ nsfw?: boolean
+ waitTranscoding?: boolean
name: string
tags?: string[]
commentsEnabled?: boolean
diff --git a/shared/models/videos/video-state.enum.ts b/shared/models/videos/video-state.enum.ts
new file mode 100644
index 000000000..625aefae1
--- /dev/null
+++ b/shared/models/videos/video-state.enum.ts
@@ -0,0 +1,4 @@
+export enum VideoState {
+ PUBLISHED = 1,
+ TO_TRANSCODE = 2
+}
diff --git a/shared/models/videos/video-update.model.ts b/shared/models/videos/video-update.model.ts
index c368d8464..681b00b18 100644
--- a/shared/models/videos/video-update.model.ts
+++ b/shared/models/videos/video-update.model.ts
@@ -11,6 +11,7 @@ export interface VideoUpdate {
tags?: string[]
commentsEnabled?: boolean
nsfw?: boolean
+ waitTranscoding?: boolean
channelId?: number
thumbnailfile?: Blob
previewfile?: Blob
diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts
index 1c86545d3..857ca1fd9 100644
--- a/shared/models/videos/video.model.ts
+++ b/shared/models/videos/video.model.ts
@@ -1,4 +1,4 @@
-import { VideoResolution } from '../../index'
+import { VideoResolution, VideoState } from '../../index'
import { Account } from '../actors'
import { Avatar } from '../avatars/avatar.model'
import { VideoChannel } from './video-channel.model'
@@ -41,6 +41,9 @@ export interface Video {
dislikes: number
nsfw: boolean
+ waitTranscoding?: boolean
+ state?: VideoConstant
+
account: {
id: number
uuid: string
@@ -70,4 +73,8 @@ export interface VideoDetails extends Video {
files: VideoFile[]
account: Account
commentsEnabled: boolean
+
+ // Not optional in details (unlike in Video)
+ waitTranscoding: boolean
+ state: VideoConstant
}
diff --git a/support/doc/api/html/index.html b/support/doc/api/html/index.html
index b75a2a8ba..e1bf61b06 100644
--- a/support/doc/api/html/index.html
+++ b/support/doc/api/html/index.html
@@ -3435,6 +3435,19 @@
Video description