<div class="video-info">
<a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
<span i18n class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>
- <div class="video-info-private">{{ video.privacy.label }}</div>
+ <div class="video-info-private">{{ video.privacy.label }} - {{ getStateLabel(video) }}</div>
</div>
<!-- Display only once -->
import { Video } from '../../shared/video/video.model'
import { VideoService } from '../../shared/video/video.service'
import { I18n } from '@ngx-translate/i18n-polyfill'
+import { VideoState } from '../../../../../shared/models/videos'
@Component({
selector: 'my-account-videos',
}
isInSelectionMode () {
- return Object.keys(this.checkedVideos).some(k => this.checkedVideos[k] === true)
+ return Object.keys(this.checkedVideos).some(k => this.checkedVideos[ k ] === true)
}
getVideosObservable (page: number) {
async deleteSelectedVideos () {
const toDeleteVideosIds = Object.keys(this.checkedVideos)
- .filter(k => this.checkedVideos[k] === true)
- .map(k => parseInt(k, 10))
+ .filter(k => this.checkedVideos[ k ] === true)
+ .map(k => parseInt(k, 10))
- const res = await this.confirmService.confirm(`Do you really want to delete ${toDeleteVideosIds.length} videos?`, 'Delete')
+ const res = await this.confirmService.confirm(
+ this.i18n('Do you really want to delete {{deleteLength}} videos?', { deleteLength: toDeleteVideosIds.length }),
+ this.i18n('Delete')
+ )
if (res === false) return
const observables: Observable<any>[] = []
for (const videoId of toDeleteVideosIds) {
- const o = this.videoService
- .removeVideo(videoId)
+ const o = this.videoService.removeVideo(videoId)
.pipe(tap(() => this.spliceVideosById(videoId)))
observables.push(o)
}
- observableFrom(observables).pipe(
- concatAll())
+ observableFrom(observables)
+ .pipe(concatAll())
.subscribe(
res => {
- this.notificationsService.success('Success', `${toDeleteVideosIds.length} videos deleted.`)
+ this.notificationsService.success(
+ this.i18n('Success'),
+ this.i18n('{{deleteLength}} videos deleted.', { deleteLength: toDeleteVideosIds.length })
+ )
+
this.abortSelectionMode()
this.reloadVideos()
},
- err => this.notificationsService.error('Error', err.message)
+ err => this.notificationsService.error(this.i18n('Error'), err.message)
)
}
async deleteVideo (video: Video) {
- const res = await this.confirmService.confirm(`Do you really want to delete ${video.name}?`, 'Delete')
+ const res = await this.confirmService.confirm(
+ this.i18n('Do you really want to delete {{videoName}}?', { videoName: video.name }),
+ this.i18n('Delete')
+ )
if (res === false) return
this.videoService.removeVideo(video.id)
- .subscribe(
- status => {
- this.notificationsService.success('Success', `Video ${video.name} deleted.`)
- this.reloadVideos()
- },
+ .subscribe(
+ status => {
+ this.notificationsService.success(
+ this.i18n('Success'),
+ this.i18n('Video {{videoName}} deleted.', { videoName: video.name })
+ )
+ this.reloadVideos()
+ },
+
+ error => this.notificationsService.error(this.i18n('Error'), error.message)
+ )
+ }
- error => this.notificationsService.error('Error', error.message)
- )
+ getStateLabel (video: Video) {
+ if (video.state.id === VideoState.PUBLISHED) return this.i18n('Published')
+
+ if (video.state.id === VideoState.TO_TRANSCODE && video.waitTranscoding === true) return this.i18n('Waiting transcoding')
+ if (video.state.id === VideoState.TO_TRANSCODE) return this.i18n('To transcode')
+
+ return this.i18n('Unknown state')
}
protected buildVideoHeight () {
private spliceVideosById (id: number) {
for (const key of Object.keys(this.loadedPages)) {
- const videos = this.loadedPages[key]
+ const videos = this.loadedPages[ key ]
const index = videos.findIndex(v => v.id === id)
if (index !== -1) {
-import { UserRight, VideoChannel, VideoDetails as VideoDetailsServerModel, VideoFile } from '../../../../../shared'
+import {
+ UserRight,
+ VideoChannel,
+ VideoConstant,
+ VideoDetails as VideoDetailsServerModel,
+ VideoFile,
+ VideoState
+} from '../../../../../shared'
import { AuthUser } from '../../core'
import { Video } from '../../shared/video/video.model'
import { Account } from '@app/shared/account/account.model'
account: Account
commentsEnabled: boolean
+ waitTranscoding: boolean
+ state: VideoConstant<VideoState>
+
likesPercent: number
dislikesPercent: number
import { VideoDetails } from './video-details.model'
import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum'
+import { VideoUpdate } from '../../../../../shared/models/videos'
-export class VideoEdit {
+export class VideoEdit implements VideoUpdate {
category: number
licence: number
language: string
tags: string[]
nsfw: boolean
commentsEnabled: boolean
+ waitTranscoding: boolean
channelId: number
privacy: VideoPrivacy
support: string
this.tags = videoDetails.tags
this.nsfw = videoDetails.nsfw
this.commentsEnabled = videoDetails.commentsEnabled
+ this.waitTranscoding = videoDetails.waitTranscoding
this.channelId = videoDetails.channel.id
this.privacy = videoDetails.privacy.id
this.support = videoDetails.support
patch (values: Object) {
Object.keys(values).forEach((key) => {
- this[key] = values[key]
+ this[ key ] = values[ key ]
})
}
tags: this.tags,
nsfw: this.nsfw,
commentsEnabled: this.commentsEnabled,
+ waitTranscoding: this.waitTranscoding,
channelId: this.channelId,
privacy: this.privacy
}
import { User } from '../'
-import { Video as VideoServerModel, VideoPrivacy } from '../../../../../shared'
+import { Video as VideoServerModel, VideoPrivacy, VideoState } from '../../../../../shared'
import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
import { VideoConstant } from '../../../../../shared/models/videos/video.model'
import { getAbsoluteAPIUrl } from '../misc/utils'
dislikes: number
nsfw: boolean
+ waitTranscoding?: boolean
+ state?: VideoConstant<VideoState>
+
account: {
id: number
uuid: string
private static createDurationString (duration: number) {
const hours = Math.floor(duration / 3600)
- const minutes = Math.floor(duration % 3600 / 60)
+ const minutes = Math.floor((duration % 3600) / 60)
const seconds = duration % 60
const minutesPadding = minutes >= 10 ? '' : '0'
const secondsPadding = seconds >= 10 ? '' : '0'
const displayedHours = hours > 0 ? hours.toString() + ':' : ''
- return displayedHours + minutesPadding +
- minutes.toString() + ':' + secondsPadding + seconds.toString()
+ return displayedHours + minutesPadding + minutes.toString() + ':' + secondsPadding + seconds.toString()
}
constructor (hash: VideoServerModel, translations = {}) {
this.licence = hash.licence
this.language = hash.language
this.privacy = hash.privacy
+ this.waitTranscoding = hash.waitTranscoding
+ this.state = hash.state
this.description = hash.description
this.duration = hash.duration
this.durationLabel = Video.createDurationString(hash.duration)
this.licence.label = peertubeTranslate(this.licence.label, translations)
this.language.label = peertubeTranslate(this.language.label, translations)
this.privacy.label = peertubeTranslate(this.privacy.label, translations)
+
+ if (this.state) this.state.label = peertubeTranslate(this.state.label, translations)
}
isVideoNSFWForUser (user: User, serverConfig: ServerConfig) {
privacy: video.privacy,
tags: video.tags,
nsfw: video.nsfw,
+ waitTranscoding: video.waitTranscoding,
commentsEnabled: video.commentsEnabled,
thumbnailfile: video.thumbnailfile,
previewfile: video.previewfile
const req = new HttpRequest('POST', VideoService.BASE_VIDEO_URL + 'upload', video, { reportProgress: true })
return this.authHttp
- .request<{ video: { id: number, uuid: string} }>(req)
+ .request<{ video: { id: number, uuid: string } }>(req)
.pipe(catchError(this.restExtractor.handleError))
}
- getMyVideos (videoPagination: ComponentPagination, sort: VideoSortField): Observable<{ videos: Video[], totalVideos: number}> {
+ getMyVideos (videoPagination: ComponentPagination, sort: VideoSortField): Observable<{ videos: Video[], totalVideos: number }> {
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
let params = new HttpParams()
account: Account,
videoPagination: ComponentPagination,
sort: VideoSortField
- ): Observable<{ videos: Video[], totalVideos: number}> {
+ ): Observable<{ videos: Video[], totalVideos: number }> {
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
let params = new HttpParams()
videoChannel: VideoChannel,
videoPagination: ComponentPagination,
sort: VideoSortField
- ): Observable<{ videos: Video[], totalVideos: number}> {
+ ): Observable<{ videos: Video[], totalVideos: number }> {
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
let params = new HttpParams()
videoPagination: ComponentPagination,
sort: VideoSortField,
filter?: VideoFilter
- ): Observable<{ videos: Video[], totalVideos: number}> {
+ ): Observable<{ videos: Video[], totalVideos: number }> {
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
let params = new HttpParams()
search: string,
videoPagination: ComponentPagination,
sort: VideoSortField
- ): Observable<{ videos: Video[], totalVideos: number}> {
+ ): Observable<{ videos: Video[], totalVideos: number }> {
const url = VideoService.BASE_VIDEO_URL + 'search'
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
private extractVideos (result: ResultList<VideoServerModel>) {
return this.serverService.localeObservable
- .pipe(
- map(translations => {
- const videosJson = result.data
- const totalVideos = result.total
- const videos: Video[] = []
-
- for (const videoJson of videosJson) {
- videos.push(new Video(videoJson, translations))
- }
-
- return { videos, totalVideos }
- })
- )
+ .pipe(
+ map(translations => {
+ const videosJson = result.data
+ const totalVideos = result.total
+ const videos: Video[] = []
+
+ for (const videoJson of videosJson) {
+ videos.push(new Video(videoJson, translations))
+ }
+
+ return { videos, totalVideos }
+ })
+ )
}
}
<label i18n for="commentsEnabled">Enable video comments</label>
</div>
+ <div class="form-group form-group-checkbox">
+ <input type="checkbox" id="waitTranscoding" formControlName="waitTranscoding" />
+ <label for="waitTranscoding"></label>
+ <label i18n for="waitTranscoding">Wait transcoding before publishing the video</label>
+ <my-help
+ tooltipPlacement="top" helpType="custom" i18n-customHtml
+ customHtml="If you decide to not wait transcoding before publishing the video, it can be unplayable until it transcoding ends."
+ ></my-help>
+ </div>
+
</div>
</tab>
const defaultValues = {
nsfw: 'false',
commentsEnabled: 'true',
+ waitTranscoding: 'true',
tags: []
}
const obj = {
channelId: this.videoValidatorsService.VIDEO_CHANNEL,
nsfw: null,
commentsEnabled: null,
+ waitTranscoding: null,
category: this.videoValidatorsService.VIDEO_CATEGORY,
licence: this.videoValidatorsService.VIDEO_LICENCE,
language: this.videoValidatorsService.VIDEO_LANGUAGE,
)
// We will update the "support" field depending on the channel
- this.form.controls['channelId']
+ this.form.controls[ 'channelId' ]
.valueChanges
.pipe(map(res => parseInt(res.toString(), 10)))
.subscribe(
newChannelId => {
- const oldChannelId = parseInt(this.form.value['channelId'], 10)
- const currentSupport = this.form.value['support']
+ const oldChannelId = parseInt(this.form.value[ 'channelId' ], 10)
+ const currentSupport = this.form.value[ 'support' ]
// Not initialized yet
if (isNaN(newChannelId)) return
const privacy = this.firstStepPrivacyId.toString()
const nsfw = false
+ const waitTranscoding = true
const commentsEnabled = true
const channelId = this.firstStepChannelId.toString()
formData.append('privacy', VideoPrivacy.PRIVATE.toString())
formData.append('nsfw', '' + nsfw)
formData.append('commentsEnabled', '' + commentsEnabled)
+ formData.append('waitTranscoding', '' + waitTranscoding)
formData.append('channelId', '' + channelId)
formData.append('videofile', videofile)
<div id="video-element-wrapper">
</div>
+ <div i18n id="warning-transcoding" class="alert alert-warning" *ngIf="isVideoToTranscode()">
+ The video is being transcoded, it may not work properly.
+ </div>
+
<!-- Video information -->
<div *ngIf="video" class="margin-content video-bottom">
<div class="video-info">
}
}
+#warning-transcoding {
+ text-align: center;
+}
+
#video-not-found {
height: 300px;
line-height: 300px;
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'
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'
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'
}
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
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))
+ })
})
}
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 () {
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) {
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 () {
this.hasAlreadyAcceptedPrivacyConcern = true
}
+ isVideoToTranscode () {
+ return this.video && this.video.state.id === VideoState.TO_TRANSCODE
+ }
+
private updateVideoDescription (description: string) {
this.video.description = description
this.setVideoDescriptionHTML()
}
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) {
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) {
}
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) {
}
},
"lint-staged": {
- "*.{css,md}": "precise-commits",
"*.scss": [
"sass-lint -c .sass-lint.yml",
"git add"
"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",
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)
}
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)
}
// 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)
}
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,
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) {
}
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
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'
} 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 {
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()
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,
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)
await video.createPreview(videoFile)
}
+ // Create the torrent file
await video.createTorrentAndSetInfoHash(videoFile)
const videoCreated = await sequelizeTypescript.transaction(async t => {
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,
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)
// 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)
import { pageToStartAndCount } from './core-utils'
function activityPubContextify <T> (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: {
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') &&
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) &&
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'
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) {
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) {
if (!video) {
res.status(404)
- .json({ error: 'Video not found' })
- .end()
+ .json({ error: 'Video not found' })
+ .end()
return false
}
isVideoTagsValid,
isVideoAbuseReasonValid,
isVideoFile,
+ isVideoStateValid,
isVideoViewsValid,
isVideoRatingTypeValid,
isVideoDurationValid,
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'
import { pseudoRandomBytesPromise } from './core-utils'
import { logger } from './logger'
+const isCidr = require('is-cidr')
+
async function generateRandomString (size: number) {
const raw = await pseudoRandomBytesPromise(size)
}
interface FormattableToJSON {
- toFormattedJSON ()
+ toFormattedJSON (args?: any)
}
-function getFormattedObjects<U, T extends FormattableToJSON> (objects: T[], objectsTotal: number) {
+function getFormattedObjects<U, T extends FormattableToJSON> (objects: T[], objectsTotal: number, formattedArg?: any) {
const formattedObjects: U[] = []
objects.forEach(object => {
- formattedObjects.push(object.toFormattedJSON())
+ formattedObjects.push(object.toFormattedJSON(formattedArg))
})
- const res: ResultList<U> = {
+ return {
total: objectsTotal,
data: formattedObjects
- }
-
- return res
+ } as ResultList<U>
}
async function isSignupAllowed () {
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)
}
}
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'
// ---------------------------------------------------------------------------
-const LAST_MIGRATION_VERSION = 215
+const LAST_MIGRATION_VERSION = 220
// ---------------------------------------------------------------------------
[VideoPrivacy.PRIVATE]: 'Private'
}
+const VIDEO_STATES = {
+ [VideoState.PUBLISHED]: 'Published',
+ [VideoState.TO_TRANSCODE]: 'To transcode'
+}
+
const VIDEO_MIMETYPE_EXT = {
'video/webm': '.webm',
'video/ogg': '.ogv',
VIDEO_LANGUAGES,
VIDEO_PRIVACIES,
VIDEO_LICENCES,
+ VIDEO_STATES,
VIDEO_RATE_TYPES,
VIDEO_MIMETYPE_EXT,
VIDEO_TRANSCODING_FPS,
--- /dev/null
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+ transaction: Sequelize.Transaction
+ queryInterface: Sequelize.QueryInterface
+ sequelize: Sequelize.Sequelize
+}): Promise<void> {
+ // 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 }
isOrigin = false
) {
const to = [ ACTIVITY_PUB.PUBLIC ]
- const cc = [ ]
+ const cc = []
// Owner of the video we comment
if (isOrigin === false) {
return actors
}
-async function getAudience (actorSender: ActorModel, t: Transaction, isPublic = true) {
+function getAudience (actorSender: ActorModel, isPublic = true) {
return buildAudience([ actorSender.followersUrl ], isPublic)
}
to = [ ACTIVITY_PUB.PUBLIC ]
cc = followerUrls
} else { // Unlisted
- to = [ ]
- cc = [ ]
+ to = []
+ cc = []
}
return { to, cc }
}
-function audiencify <T> (object: T, audience: ActivityAudience) {
+function audiencify<T> (object: T, audience: ActivityAudience) {
return Object.assign(object, audience)
}
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)
}
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'
import { VideoFileModel } from '../../../models/video/video-file'
import { fetchAvatarIfExists, getOrCreateActorAndServerAndModel, updateActorAvatarInstance, updateActorInstance } from '../actor'
import {
+ fetchRemoteVideo,
generateThumbnailFromUrl,
getOrCreateAccountAndVideoAndChannel,
getOrCreateVideoChannel,
}
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
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)
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)
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<void>[] = []
}
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)
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) {
return broadcastToFollowers(data, byActor, [ byActor ], t)
}
-async function announceActivityData (
- url: string,
- byActor: ActorModel,
- object: string,
- t: Transaction,
- audience?: ActivityAudience
-): Promise<ActivityAnnounce> {
- 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',
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)
}
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)
}
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 ]
// 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()
// 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<ActivityCreate> {
- if (!audience) {
- audience = await getAudience(byActor, t)
- }
-
- return audiencify({
- type: 'Create' as 'Create',
- id: url + '/activity',
- actor: byActor.url,
- object: audiencify(object, audience)
- }, audience)
+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
+ )
}
function createDislikeActivityData (byActor: ActorModel, video: VideoModel) {
// 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<ActivityLike> {
- if (!audience) {
- audience = await getAudience(byActor, t)
- }
-
- return audiencify({
- type: 'Like' as 'Like',
- id: url,
- actor: byActor.url,
- object: video.url
- }, audience)
+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
+ )
}
// ---------------------------------------------------------------------------
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)
}
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)
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)
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)
// ---------------------------------------------------------------------------
-async function undoActivityData (
+function undoActivityData (
url: string,
byActor: ActorModel,
object: ActivityFollow | ActivityLike | ActivityCreate | ActivityAnnounce,
- t: Transaction,
audience?: ActivityAudience
-): Promise<ActivityUndo> {
- if (!audience) {
- audience = await getAudience(byActor, t)
- }
-
- return audiencify({
- type: 'Undo' as 'Undo',
- id: url,
- actor: byActor.url,
- object
- }, audience)
+): ActivityUndo {
+ if (!audience) audience = getAudience(byActor)
+
+ return audiencify(
+ {
+ type: 'Undo' as 'Undo',
+ id: url,
+ actor: byActor.url,
+ object
+ },
+ audience
+ )
}
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)
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) {
// ---------------------------------------------------------------------------
-async function updateActivityData (
- url: string,
- byActor: ActorModel,
- object: any,
- t: Transaction,
- audience?: ActivityAudience
-): Promise<ActivityUpdate> {
- 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
+ )
}
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'
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
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]+/, '')
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),
}
async function getOrCreateAccountAndVideoAndChannel (videoObject: VideoTorrentObject | string, actor?: ActorModel) {
- if (typeof videoObject === 'string') {
- const videoUrl = videoObject
-
- const videoFromDatabase = await VideoModel.loadByUrlAndPopulateAccount(videoUrl)
- if (videoFromDatabase) {
- return {
- video: videoFromDatabase,
- actor: videoFromDatabase.VideoChannel.Account.Actor,
- channelActor: videoFromDatabase.VideoChannel.Actor
- }
+ 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
}
-
- 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)
}
}
-export {
- getOrCreateAccountAndVideoAndChannel,
- fetchRemoteVideoPreview,
- fetchRemoteVideoDescription,
- generateThumbnailFromUrl,
- videoActivityObjectToDBAttributes,
- videoFileActivityUrlToDBAttributes,
- getOrCreateVideo,
- getOrCreateVideoChannel,
- addVideoShares
-}
-
-// ---------------------------------------------------------------------------
-
async function fetchRemoteVideo (videoUrl: string): Promise<VideoTorrentObject> {
const options = {
uri: videoUrl,
return body
}
+
+export {
+ federateVideoIfNeeded,
+ fetchRemoteVideo,
+ getOrCreateAccountAndVideoAndChannel,
+ fetchRemoteVideoPreview,
+ fetchRemoteVideoDescription,
+ generateThumbnailFromUrl,
+ videoActivityObjectToDBAttributes,
+ videoFileActivityUrlToDBAttributes,
+ getOrCreateVideo,
+ getOrCreateVideoChannel,
+ addVideoShares
+}
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
}
// 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
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 })
+
+ // 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
+ 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
-
- 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)
- }
- }
-
- 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 }
- )
+ // Outside the transaction (IO on disk)
+ const { videoFileResolution } = await video.getOriginalFileResolution()
+
+ 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
+
+ // 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<any>[] = []
+
+ for (const resolution of resolutionsEnabled) {
+ const dataInput = {
+ videoUUID: videoDatabase.uuid,
+ resolution
+ }
+
+ const p = JobQueue.Instance.createJob({ type: 'video-file', payload: dataInput })
+ tasks.push(p)
+ }
- if (resolutionsEnabled.length !== 0) {
- const tasks: Promise<any>[] = []
+ await Promise.all(tasks)
- for (const resolution of resolutionsEnabled) {
- const dataInput = {
- videoUUID: videoDatabase.uuid,
- resolution,
- isNewVideo
- }
+ 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 })
- const p = JobQueue.Instance.createJob({ type: 'video-file', payload: dataInput })
- tasks.push(p)
+ 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)
+ })
}
// ---------------------------------------------------------------------------
const res = await handlers[ handlerName ](job)
return done(null, res)
} catch (err) {
+ logger.error('Cannot execute job %d.', job.id, { err })
return done(err)
}
})
// 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)
.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)
.customSanitizer(toValueOrNull)
.custom(isVideoTagsValid).withMessage('Should have correct tags'),
body('commentsEnabled')
+ .optional()
.toBoolean()
.custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
body('privacy')
.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()
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'
isVideoLanguageValid,
isVideoLicenceValid,
isVideoNameValid,
- isVideoPrivacyValid,
+ isVideoPrivacyValid, isVideoStateValid,
isVideoSupportValid
} from '../../helpers/custom-validators/videos'
import { generateImageFromVideoFile, getVideoFileResolution, transcode } from '../../helpers/ffmpeg-utils'
VIDEO_EXT_MIMETYPE,
VIDEO_LANGUAGES,
VIDEO_LICENCES,
- VIDEO_PRIVACIES
+ VIDEO_PRIVACIES, VIDEO_STATES
} from '../../initializers'
import {
getVideoCommentsActivityPubUrl,
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({
')'
)
},
- 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 ]
}
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({
fields: [ 'channelId' ]
},
{
- fields: [ 'id', 'privacy' ]
+ fields: [ 'id', 'privacy', 'state', 'waitTranscoding' ]
},
{
fields: [ 'url'],
@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
})
}
- 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<VideoModel> = {
offset: start,
limit: count,
.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
}
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 () {
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,
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 {
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: []
}
language,
views: this.views,
sensitive: this.nsfw,
+ waitTranscoding: this.waitTranscoding,
+ state: this.state,
commentsEnabled: this.commentsEnabled,
published: this.publishedAt.toISOString(),
updated: this.updatedAt.toISOString(),
language: 'pt',
nsfw: false,
commentsEnabled: true,
+ waitTranscoding: true,
description: 'my super description',
support: 'my super support text',
tags: [ 'tag1', 'tag2' ],
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
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'
.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)
},
isLocal,
duration: 5,
- commentsEnabled: true,
+ commentsEnabled: false,
tags: [ ],
privacy: VideoPrivacy.PUBLIC,
channel: {
const oembedUrl = 'http://localhost:9001/videos/watch/' + server.video.uuid
const res = await getOEmbed(server.url, oembedUrl)
- const expectedHtml = `<iframe width="560" height="315" src="http://localhost:9001/videos/embed/${server.video.uuid}" ` +
+ const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' +
+ `src="http://localhost:9001/videos/embed/${server.video.uuid}" ` +
'frameborder="0" allowfullscreen></iframe>'
const expectedThumbnailUrl = 'http://localhost:9001/static/previews/' + server.video.uuid + '.jpg'
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'
}
})
+ 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)
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)
language?: string
nsfw?: boolean
commentsEnabled?: boolean
+ waitTranscoding?: boolean
description?: string
tags?: string[]
channelId?: number
language: 'zh',
channelId: defaultChannelId,
nsfw: true,
+ waitTranscoding: false,
description: 'my super description',
support: 'my super support text',
tags: [ 'tag' ],
.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)
licence,
language,
nsfw: isNSFW(videoInfo),
+ waitTranscoding: true,
commentsEnabled: true,
description: videoInfo.description || undefined,
support: undefined,
fixture: program['file'],
thumbnailfile: program['thumbnailPath'],
previewfile: program['previewPath'],
+ waitTranscoding: true,
privacy: program['privacy'],
support: undefined
}
ActivityUrlObject
} from './common-objects'
import { ActivityPubOrderedCollection } from '../activitypub-ordered-collection'
+import { VideoState } from '../../videos'
export interface VideoTorrentObject {
type: 'Video'
views: number
sensitive: boolean
commentsEnabled: boolean
+ waitTranscoding: boolean
+ state: VideoState
published: string
updated: string
mediaType: 'text/markdown'
export * from './video-resolution.enum'
export * from './video-update.model'
export * from './video.model'
+export * from './video-state.enum'
description?: string
support?: string
channelId: number
- nsfw: boolean
+ nsfw?: boolean
+ waitTranscoding?: boolean
name: string
tags?: string[]
commentsEnabled?: boolean
--- /dev/null
+export enum VideoState {
+ PUBLISHED = 1,
+ TO_TRANSCODE = 2
+}
tags?: string[]
commentsEnabled?: boolean
nsfw?: boolean
+ waitTranscoding?: boolean
channelId?: number
thumbnailfile?: Blob
previewfile?: Blob
-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'
dislikes: number
nsfw: boolean
+ waitTranscoding?: boolean
+ state?: VideoConstant<VideoState>
+
account: {
id: number
uuid: string
files: VideoFile[]
account: Account
commentsEnabled: boolean
+
+ // Not optional in details (unlike in Video)
+ waitTranscoding: boolean
+ state: VideoConstant<VideoState>
}
<p>Video description</p>
</div>
</div>
+ <div class="prop-row prop-group">
+ <div class="prop-name">
+ <div class="prop-title">waitTranscoding</div>
+ <div class="prop-subtitle"> in formData </div>
+ <div class="prop-subtitle">
+ <span class="json-property-type">boolean</span>
+ <span class="json-property-range" title="Value limits"></span>
+ </div>
+ </div>
+ <div class="prop-value">
+ <p>Whether or not we wait transcoding before publish the video</p>
+ </div>
+ </div>
<div class="prop-row prop-group">
<div class="prop-name">
<div class="prop-title">support</div>
<p>Video category</p>
</div>
</div>
+ <div class="prop-row prop-group">
+ <div class="prop-name">
+ <div class="prop-title">waitTranscoding</div>
+ <div class="prop-subtitle"> in formData </div>
+ <div class="prop-subtitle">
+ <span class="json-property-type">boolean</span>
+ <span class="json-property-range" title="Value limits"></span>
+ </div>
+ </div>
+ <div class="prop-value">
+ <p>Whether or not we wait transcoding before publish the video</p>
+ </div>
+ </div>
<div class="prop-row prop-group">
<div class="prop-name">
<div class="prop-title">licence</div>
in: formData
type: string
description: 'Video description'
+ - name: waitTranscoding
+ in: formData
+ type: boolean
+ description: 'Whether or not we wait transcoding before publish the video'
- name: support
in: formData
type: string
in: formData
type: number
description: 'Video category'
+ - name: waitTranscoding
+ in: formData
+ type: boolean
+ description: 'Whether or not we wait transcoding before publish the video'
- name: licence
in: formData
type: number
* Vimeo: https://vimeo.com/xxxxxx
* Dailymotion: https://www.dailymotion.com/xxxxx
- The script will get all public videos from Youtube, download them and upload to PeerTube.
- Already downloaded videos will not be uploaded twice, so you can run and re-run the script in case of crash, disconnection...
+The script will get all public videos from Youtube, download them and upload to PeerTube.
+Already downloaded videos will not be uploaded twice, so you can run and re-run the script in case of crash, disconnection...
+
+Videos will be publicly available after transcoding (you can see them before that in your account on the web interface).
+
### upload.js
You can use this script to import videos directly from the CLI.
+Videos will be publicly available after transcoding (you can see them before that in your account on the web interface).
+
```
$ cd ${CLONE}
$ node dist/server/tools/upload.js --help