1
0
Fork 0

Add concept of video state, and add ability to wait transcoding before

publishing a video
This commit is contained in:
Chocobozzz 2018-06-12 20:04:58 +02:00
parent 6ccdf3a23e
commit 2186386cca
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
54 changed files with 762 additions and 476 deletions

View File

@ -18,7 +18,7 @@
<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 -->

View File

@ -12,6 +12,7 @@ import { AbstractVideoList } from '../../shared/video/abstract-video-list'
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',
@ -59,7 +60,7 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
}
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) {
@ -74,47 +75,68 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
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('Error', error.message)
)
error => this.notificationsService.error(this.i18n('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 () {
@ -124,7 +146,7 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
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) {

View File

@ -1,4 +1,11 @@
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'
@ -12,6 +19,9 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
account: Account
commentsEnabled: boolean
waitTranscoding: boolean
state: VideoConstant<VideoState>
likesPercent: number
dislikesPercent: number

View File

@ -1,7 +1,8 @@
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
@ -10,6 +11,7 @@ export class VideoEdit {
tags: string[]
nsfw: boolean
commentsEnabled: boolean
waitTranscoding: boolean
channelId: number
privacy: VideoPrivacy
support: string
@ -32,6 +34,7 @@ export class VideoEdit {
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
@ -42,7 +45,7 @@ export class VideoEdit {
patch (values: Object) {
Object.keys(values).forEach((key) => {
this[key] = values[key]
this[ key ] = values[ key ]
})
}
@ -57,6 +60,7 @@ export class VideoEdit {
tags: this.tags,
nsfw: this.nsfw,
commentsEnabled: this.commentsEnabled,
waitTranscoding: this.waitTranscoding,
channelId: this.channelId,
privacy: this.privacy
}

View File

@ -1,5 +1,5 @@
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'
@ -36,6 +36,9 @@ export class Video implements VideoServerModel {
dislikes: number
nsfw: boolean
waitTranscoding?: boolean
state?: VideoConstant<VideoState>
account: {
id: number
uuid: string
@ -58,15 +61,14 @@ export class Video implements VideoServerModel {
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 = {}) {
@ -78,6 +80,8 @@ export class Video implements VideoServerModel {
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)
@ -104,6 +108,8 @@ export class Video implements VideoServerModel {
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) {

View File

@ -80,6 +80,7 @@ export class VideoService {
privacy: video.privacy,
tags: video.tags,
nsfw: video.nsfw,
waitTranscoding: video.waitTranscoding,
commentsEnabled: video.commentsEnabled,
thumbnailfile: video.thumbnailfile,
previewfile: video.previewfile
@ -98,11 +99,11 @@ export class VideoService {
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()
@ -120,7 +121,7 @@ export class VideoService {
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()
@ -138,7 +139,7 @@ export class VideoService {
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()
@ -156,7 +157,7 @@ export class VideoService {
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()
@ -225,7 +226,7 @@ export class VideoService {
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)
@ -295,18 +296,18 @@ export class VideoService {
private extractVideos (result: ResultList<VideoServerModel>) {
return this.serverService.localeObservable
.pipe(
map(translations => {
const videosJson = result.data
const totalVideos = result.total
const videos: Video[] = []
.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))
}
for (const videoJson of videosJson) {
videos.push(new Video(videoJson, translations))
}
return { videos, totalVideos }
})
)
return { videos, totalVideos }
})
)
}
}

View File

@ -109,6 +109,16 @@
<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>

View File

@ -47,6 +47,7 @@ export class VideoEditComponent implements OnInit {
const defaultValues = {
nsfw: 'false',
commentsEnabled: 'true',
waitTranscoding: 'true',
tags: []
}
const obj = {
@ -55,6 +56,7 @@ export class VideoEditComponent implements OnInit {
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,
@ -74,13 +76,13 @@ export class VideoEditComponent implements OnInit {
)
// 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

View File

@ -164,6 +164,7 @@ export class VideoAddComponent extends FormReactive implements OnInit, OnDestroy
const privacy = this.firstStepPrivacyId.toString()
const nsfw = false
const waitTranscoding = true
const commentsEnabled = true
const channelId = this.firstStepChannelId.toString()
@ -173,6 +174,7 @@ export class VideoAddComponent extends FormReactive implements OnInit, OnDestroy
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)

View File

@ -3,6 +3,10 @@
<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">

View File

@ -28,6 +28,10 @@
}
}
#warning-transcoding {
text-align: center;
}
#video-not-found {
height: 300px;
line-height: 300px;

View File

@ -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) {

View File

@ -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",

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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

View File

@ -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)

View File

@ -8,22 +8,24 @@ import { signObject } from './peertube-crypto'
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: {

View File

@ -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) &&

View File

@ -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,

View File

@ -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<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 () {
@ -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)
}
}

View File

@ -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,

View File

@ -0,0 +1,62 @@
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 }

View File

@ -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 <T> (object: T, audience: ActivityAudience) {
function audiencify<T> (object: T, audience: ActivityAudience) {
return Object.assign(object, audience)
}

View File

@ -28,7 +28,7 @@ async function crawlCollectionPage <T> (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)
}

View File

@ -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<void>[] = []
@ -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)

View File

@ -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<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',

View File

@ -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<ActivityCreate> {
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) {

View File

@ -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<ActivityLike> {
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
)
}
// ---------------------------------------------------------------------------

View File

@ -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<ActivityUndo> {
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
)
}

View File

@ -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<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
)
}

View File

@ -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<VideoTorrentObject> {
const options = {
uri: videoUrl,
@ -324,3 +328,17 @@ async function fetchRemoteVideo (videoUrl: string): Promise<VideoTorrentObject>
return body
}
export {
federateVideoIfNeeded,
fetchRemoteVideo,
getOrCreateAccountAndVideoAndChannel,
fetchRemoteVideoPreview,
fetchRemoteVideoDescription,
generateThumbnailFromUrl,
videoActivityObjectToDBAttributes,
videoFileActivityUrlToDBAttributes,
getOrCreateVideo,
getOrCreateVideoChannel,
addVideoShares
}

View File

@ -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<any>[] = []
if (resolutionsEnabled.length !== 0) {
const tasks: Promise<any>[] = []
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)
})
}
// ---------------------------------------------------------------------------

View File

@ -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)
}
})

View File

@ -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)

View File

@ -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()

View File

@ -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<VideoModel> {
@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<VideoModel> {
})
}
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,
@ -858,12 +842,13 @@ export class VideoModel extends Model<VideoModel> {
.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<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 () {
@ -1026,11 +1003,16 @@ export class VideoModel extends Model<VideoModel> {
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<VideoModel> {
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<VideoModel> {
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<VideoModel> {
language,
views: this.views,
sensitive: this.nsfw,
waitTranscoding: this.waitTranscoding,
state: this.state,
commentsEnabled: this.commentsEnabled,
published: this.publishedAt.toISOString(),
updated: this.updatedAt.toISOString(),

View File

@ -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

View File

@ -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: {

View File

@ -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 = `<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'

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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,

View File

@ -84,6 +84,7 @@ async function run () {
fixture: program['file'],
thumbnailfile: program['thumbnailPath'],
previewfile: program['previewPath'],
waitTranscoding: true,
privacy: program['privacy'],
support: undefined
}

View File

@ -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'

View File

@ -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'

View File

@ -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

View File

@ -0,0 +1,4 @@
export enum VideoState {
PUBLISHED = 1,
TO_TRANSCODE = 2
}

View File

@ -11,6 +11,7 @@ export interface VideoUpdate {
tags?: string[]
commentsEnabled?: boolean
nsfw?: boolean
waitTranscoding?: boolean
channelId?: number
thumbnailfile?: Blob
previewfile?: Blob

View File

@ -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<VideoState>
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<VideoState>
}

View File

@ -3435,6 +3435,19 @@
<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>
@ -4009,6 +4022,19 @@
<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>

View File

@ -682,6 +682,10 @@ paths:
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
@ -814,6 +818,10 @@ paths:
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

View File

@ -63,13 +63,18 @@ $ node dist/server/tools/import-videos.js \
* 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