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"> <div class="video-info">
<a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a> <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> <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> </div>
<!-- Display only once --> <!-- 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 { Video } from '../../shared/video/video.model'
import { VideoService } from '../../shared/video/video.service' import { VideoService } from '../../shared/video/video.service'
import { I18n } from '@ngx-translate/i18n-polyfill' import { I18n } from '@ngx-translate/i18n-polyfill'
import { VideoState } from '../../../../../shared/models/videos'
@Component({ @Component({
selector: 'my-account-videos', selector: 'my-account-videos',
@ -59,7 +60,7 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
} }
isInSelectionMode () { 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) { getVideosObservable (page: number) {
@ -74,47 +75,68 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
async deleteSelectedVideos () { async deleteSelectedVideos () {
const toDeleteVideosIds = Object.keys(this.checkedVideos) const toDeleteVideosIds = Object.keys(this.checkedVideos)
.filter(k => this.checkedVideos[k] === true) .filter(k => this.checkedVideos[ k ] === true)
.map(k => parseInt(k, 10)) .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 if (res === false) return
const observables: Observable<any>[] = [] const observables: Observable<any>[] = []
for (const videoId of toDeleteVideosIds) { for (const videoId of toDeleteVideosIds) {
const o = this.videoService const o = this.videoService.removeVideo(videoId)
.removeVideo(videoId)
.pipe(tap(() => this.spliceVideosById(videoId))) .pipe(tap(() => this.spliceVideosById(videoId)))
observables.push(o) observables.push(o)
} }
observableFrom(observables).pipe( observableFrom(observables)
concatAll()) .pipe(concatAll())
.subscribe( .subscribe(
res => { 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.abortSelectionMode()
this.reloadVideos() this.reloadVideos()
}, },
err => this.notificationsService.error('Error', err.message) err => this.notificationsService.error(this.i18n('Error'), err.message)
) )
} }
async deleteVideo (video: Video) { 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 if (res === false) return
this.videoService.removeVideo(video.id) this.videoService.removeVideo(video.id)
.subscribe( .subscribe(
status => { status => {
this.notificationsService.success('Success', `Video ${video.name} deleted.`) this.notificationsService.success(
this.reloadVideos() 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 () { protected buildVideoHeight () {
@ -124,7 +146,7 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
private spliceVideosById (id: number) { private spliceVideosById (id: number) {
for (const key of Object.keys(this.loadedPages)) { 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) const index = videos.findIndex(v => v.id === id)
if (index !== -1) { 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 { AuthUser } from '../../core'
import { Video } from '../../shared/video/video.model' import { Video } from '../../shared/video/video.model'
import { Account } from '@app/shared/account/account.model' import { Account } from '@app/shared/account/account.model'
@ -12,6 +19,9 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
account: Account account: Account
commentsEnabled: boolean commentsEnabled: boolean
waitTranscoding: boolean
state: VideoConstant<VideoState>
likesPercent: number likesPercent: number
dislikesPercent: number dislikesPercent: number

View File

@ -1,7 +1,8 @@
import { VideoDetails } from './video-details.model' import { VideoDetails } from './video-details.model'
import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum' 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 category: number
licence: number licence: number
language: string language: string
@ -10,6 +11,7 @@ export class VideoEdit {
tags: string[] tags: string[]
nsfw: boolean nsfw: boolean
commentsEnabled: boolean commentsEnabled: boolean
waitTranscoding: boolean
channelId: number channelId: number
privacy: VideoPrivacy privacy: VideoPrivacy
support: string support: string
@ -32,6 +34,7 @@ export class VideoEdit {
this.tags = videoDetails.tags this.tags = videoDetails.tags
this.nsfw = videoDetails.nsfw this.nsfw = videoDetails.nsfw
this.commentsEnabled = videoDetails.commentsEnabled this.commentsEnabled = videoDetails.commentsEnabled
this.waitTranscoding = videoDetails.waitTranscoding
this.channelId = videoDetails.channel.id this.channelId = videoDetails.channel.id
this.privacy = videoDetails.privacy.id this.privacy = videoDetails.privacy.id
this.support = videoDetails.support this.support = videoDetails.support
@ -42,7 +45,7 @@ export class VideoEdit {
patch (values: Object) { patch (values: Object) {
Object.keys(values).forEach((key) => { Object.keys(values).forEach((key) => {
this[key] = values[key] this[ key ] = values[ key ]
}) })
} }
@ -57,6 +60,7 @@ export class VideoEdit {
tags: this.tags, tags: this.tags,
nsfw: this.nsfw, nsfw: this.nsfw,
commentsEnabled: this.commentsEnabled, commentsEnabled: this.commentsEnabled,
waitTranscoding: this.waitTranscoding,
channelId: this.channelId, channelId: this.channelId,
privacy: this.privacy privacy: this.privacy
} }

View File

@ -1,5 +1,5 @@
import { User } from '../' 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 { Avatar } from '../../../../../shared/models/avatars/avatar.model'
import { VideoConstant } from '../../../../../shared/models/videos/video.model' import { VideoConstant } from '../../../../../shared/models/videos/video.model'
import { getAbsoluteAPIUrl } from '../misc/utils' import { getAbsoluteAPIUrl } from '../misc/utils'
@ -36,6 +36,9 @@ export class Video implements VideoServerModel {
dislikes: number dislikes: number
nsfw: boolean nsfw: boolean
waitTranscoding?: boolean
state?: VideoConstant<VideoState>
account: { account: {
id: number id: number
uuid: string uuid: string
@ -58,15 +61,14 @@ export class Video implements VideoServerModel {
private static createDurationString (duration: number) { private static createDurationString (duration: number) {
const hours = Math.floor(duration / 3600) 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 seconds = duration % 60
const minutesPadding = minutes >= 10 ? '' : '0' const minutesPadding = minutes >= 10 ? '' : '0'
const secondsPadding = seconds >= 10 ? '' : '0' const secondsPadding = seconds >= 10 ? '' : '0'
const displayedHours = hours > 0 ? hours.toString() + ':' : '' const displayedHours = hours > 0 ? hours.toString() + ':' : ''
return displayedHours + minutesPadding + return displayedHours + minutesPadding + minutes.toString() + ':' + secondsPadding + seconds.toString()
minutes.toString() + ':' + secondsPadding + seconds.toString()
} }
constructor (hash: VideoServerModel, translations = {}) { constructor (hash: VideoServerModel, translations = {}) {
@ -78,6 +80,8 @@ export class Video implements VideoServerModel {
this.licence = hash.licence this.licence = hash.licence
this.language = hash.language this.language = hash.language
this.privacy = hash.privacy this.privacy = hash.privacy
this.waitTranscoding = hash.waitTranscoding
this.state = hash.state
this.description = hash.description this.description = hash.description
this.duration = hash.duration this.duration = hash.duration
this.durationLabel = Video.createDurationString(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.licence.label = peertubeTranslate(this.licence.label, translations)
this.language.label = peertubeTranslate(this.language.label, translations) this.language.label = peertubeTranslate(this.language.label, translations)
this.privacy.label = peertubeTranslate(this.privacy.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) { isVideoNSFWForUser (user: User, serverConfig: ServerConfig) {

View File

@ -80,6 +80,7 @@ export class VideoService {
privacy: video.privacy, privacy: video.privacy,
tags: video.tags, tags: video.tags,
nsfw: video.nsfw, nsfw: video.nsfw,
waitTranscoding: video.waitTranscoding,
commentsEnabled: video.commentsEnabled, commentsEnabled: video.commentsEnabled,
thumbnailfile: video.thumbnailfile, thumbnailfile: video.thumbnailfile,
previewfile: video.previewfile previewfile: video.previewfile
@ -98,11 +99,11 @@ export class VideoService {
const req = new HttpRequest('POST', VideoService.BASE_VIDEO_URL + 'upload', video, { reportProgress: true }) const req = new HttpRequest('POST', VideoService.BASE_VIDEO_URL + 'upload', video, { reportProgress: true })
return this.authHttp return this.authHttp
.request<{ video: { id: number, uuid: string} }>(req) .request<{ video: { id: number, uuid: string } }>(req)
.pipe(catchError(this.restExtractor.handleError)) .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) const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
let params = new HttpParams() let params = new HttpParams()
@ -120,7 +121,7 @@ export class VideoService {
account: Account, account: Account,
videoPagination: ComponentPagination, videoPagination: ComponentPagination,
sort: VideoSortField sort: VideoSortField
): Observable<{ videos: Video[], totalVideos: number}> { ): Observable<{ videos: Video[], totalVideos: number }> {
const pagination = this.restService.componentPaginationToRestPagination(videoPagination) const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
let params = new HttpParams() let params = new HttpParams()
@ -138,7 +139,7 @@ export class VideoService {
videoChannel: VideoChannel, videoChannel: VideoChannel,
videoPagination: ComponentPagination, videoPagination: ComponentPagination,
sort: VideoSortField sort: VideoSortField
): Observable<{ videos: Video[], totalVideos: number}> { ): Observable<{ videos: Video[], totalVideos: number }> {
const pagination = this.restService.componentPaginationToRestPagination(videoPagination) const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
let params = new HttpParams() let params = new HttpParams()
@ -156,7 +157,7 @@ export class VideoService {
videoPagination: ComponentPagination, videoPagination: ComponentPagination,
sort: VideoSortField, sort: VideoSortField,
filter?: VideoFilter filter?: VideoFilter
): Observable<{ videos: Video[], totalVideos: number}> { ): Observable<{ videos: Video[], totalVideos: number }> {
const pagination = this.restService.componentPaginationToRestPagination(videoPagination) const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
let params = new HttpParams() let params = new HttpParams()
@ -225,7 +226,7 @@ export class VideoService {
search: string, search: string,
videoPagination: ComponentPagination, videoPagination: ComponentPagination,
sort: VideoSortField sort: VideoSortField
): Observable<{ videos: Video[], totalVideos: number}> { ): Observable<{ videos: Video[], totalVideos: number }> {
const url = VideoService.BASE_VIDEO_URL + 'search' const url = VideoService.BASE_VIDEO_URL + 'search'
const pagination = this.restService.componentPaginationToRestPagination(videoPagination) const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
@ -295,18 +296,18 @@ export class VideoService {
private extractVideos (result: ResultList<VideoServerModel>) { private extractVideos (result: ResultList<VideoServerModel>) {
return this.serverService.localeObservable return this.serverService.localeObservable
.pipe( .pipe(
map(translations => { map(translations => {
const videosJson = result.data const videosJson = result.data
const totalVideos = result.total const totalVideos = result.total
const videos: Video[] = [] const videos: Video[] = []
for (const videoJson of videosJson) { for (const videoJson of videosJson) {
videos.push(new Video(videoJson, translations)) 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> <label i18n for="commentsEnabled">Enable video comments</label>
</div> </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> </div>
</tab> </tab>

View File

@ -47,6 +47,7 @@ export class VideoEditComponent implements OnInit {
const defaultValues = { const defaultValues = {
nsfw: 'false', nsfw: 'false',
commentsEnabled: 'true', commentsEnabled: 'true',
waitTranscoding: 'true',
tags: [] tags: []
} }
const obj = { const obj = {
@ -55,6 +56,7 @@ export class VideoEditComponent implements OnInit {
channelId: this.videoValidatorsService.VIDEO_CHANNEL, channelId: this.videoValidatorsService.VIDEO_CHANNEL,
nsfw: null, nsfw: null,
commentsEnabled: null, commentsEnabled: null,
waitTranscoding: null,
category: this.videoValidatorsService.VIDEO_CATEGORY, category: this.videoValidatorsService.VIDEO_CATEGORY,
licence: this.videoValidatorsService.VIDEO_LICENCE, licence: this.videoValidatorsService.VIDEO_LICENCE,
language: this.videoValidatorsService.VIDEO_LANGUAGE, language: this.videoValidatorsService.VIDEO_LANGUAGE,
@ -74,13 +76,13 @@ export class VideoEditComponent implements OnInit {
) )
// We will update the "support" field depending on the channel // We will update the "support" field depending on the channel
this.form.controls['channelId'] this.form.controls[ 'channelId' ]
.valueChanges .valueChanges
.pipe(map(res => parseInt(res.toString(), 10))) .pipe(map(res => parseInt(res.toString(), 10)))
.subscribe( .subscribe(
newChannelId => { newChannelId => {
const oldChannelId = parseInt(this.form.value['channelId'], 10) const oldChannelId = parseInt(this.form.value[ 'channelId' ], 10)
const currentSupport = this.form.value['support'] const currentSupport = this.form.value[ 'support' ]
// Not initialized yet // Not initialized yet
if (isNaN(newChannelId)) return if (isNaN(newChannelId)) return

View File

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

View File

@ -3,6 +3,10 @@
<div id="video-element-wrapper"> <div id="video-element-wrapper">
</div> </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 --> <!-- Video information -->
<div *ngIf="video" class="margin-content video-bottom"> <div *ngIf="video" class="margin-content video-bottom">
<div class="video-info"> <div class="video-info">

View File

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

View File

@ -1,5 +1,5 @@
import { catchError } from 'rxjs/operators' 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 { ActivatedRoute, Router } from '@angular/router'
import { RedirectService } from '@app/core/routing/redirect.service' import { RedirectService } from '@app/core/routing/redirect.service'
import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage'
@ -10,7 +10,7 @@ import { Subscription } from 'rxjs'
import * as videojs from 'video.js' import * as videojs from 'video.js'
import 'videojs-hotkeys' import 'videojs-hotkeys'
import * as WebTorrent from 'webtorrent' import * as WebTorrent from 'webtorrent'
import { UserVideoRateType, VideoRateType } from '../../../../../shared' import { UserVideoRateType, VideoRateType, VideoState } from '../../../../../shared'
import '../../../assets/player/peertube-videojs-plugin' import '../../../assets/player/peertube-videojs-plugin'
import { AuthService, ConfirmService } from '../../core' import { AuthService, ConfirmService } from '../../core'
import { RestExtractor, VideoBlacklistService } from '../../shared' import { RestExtractor, VideoBlacklistService } from '../../shared'
@ -21,7 +21,7 @@ import { MarkdownService } from '../shared'
import { VideoDownloadComponent } from './modal/video-download.component' import { VideoDownloadComponent } from './modal/video-download.component'
import { VideoReportComponent } from './modal/video-report.component' import { VideoReportComponent } from './modal/video-report.component'
import { VideoShareComponent } from './modal/video-share.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 { ServerService } from '@app/core'
import { I18n } from '@ngx-translate/i18n-polyfill' import { I18n } from '@ngx-translate/i18n-polyfill'
import { environment } from '../../../environments/environment' import { environment } from '../../../environments/environment'
@ -91,21 +91,21 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
} }
this.videoService.getVideos({ currentPage: 1, itemsPerPage: 5 }, '-createdAt') this.videoService.getVideos({ currentPage: 1, itemsPerPage: 5 }, '-createdAt')
.subscribe( .subscribe(
data => { data => {
this.otherVideos = data.videos this.otherVideos = data.videos
this.updateOtherVideosDisplayed() this.updateOtherVideosDisplayed()
}, },
err => console.error(err) err => console.error(err)
) )
this.paramsSub = this.route.params.subscribe(routeParams => { this.paramsSub = this.route.params.subscribe(routeParams => {
if (this.player) { if (this.player) {
this.player.pause() this.player.pause()
} }
const uuid = routeParams['uuid'] const uuid = routeParams[ 'uuid' ]
// Video did not change // Video did not change
if (this.video && this.video.uuid === uuid) return if (this.video && this.video.uuid === uuid) return
@ -113,13 +113,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.videoService this.videoService
.getVideo(uuid) .getVideo(uuid)
.pipe(catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 404 ]))) .pipe(catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 404 ])))
.subscribe( .subscribe(video => {
video => { const startTime = this.route.snapshot.queryParams.start
const startTime = this.route.snapshot.queryParams.start this.onVideoFetched(video, startTime)
this.onVideoFetched(video, startTime) .catch(err => this.handleError(err))
.catch(err => this.handleError(err)) })
}
)
}) })
} }
@ -157,17 +155,17 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
if (res === false) return if (res === false) return
this.videoBlacklistService.blacklistVideo(this.video.id) this.videoBlacklistService.blacklistVideo(this.video.id)
.subscribe( .subscribe(
status => { () => {
this.notificationsService.success( this.notificationsService.success(
this.i18n('Success'), this.i18n('Success'),
this.i18n('Video {{videoName}} had been blacklisted.', { videoName: this.video.name }) this.i18n('Video {{videoName}} had been blacklisted.', { videoName: this.video.name })
) )
this.redirectService.redirectToHomepage() this.redirectService.redirectToHomepage()
}, },
error => this.notificationsService.error(this.i18n('Error'), error.message) error => this.notificationsService.error(this.i18n('Error'), error.message)
) )
} }
showMoreDescription () { showMoreDescription () {
@ -188,22 +186,22 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.descriptionLoading = true this.descriptionLoading = true
this.videoService.loadCompleteDescription(this.video.descriptionPath) this.videoService.loadCompleteDescription(this.video.descriptionPath)
.subscribe( .subscribe(
description => { description => {
this.completeDescriptionShown = true this.completeDescriptionShown = true
this.descriptionLoading = false this.descriptionLoading = false
this.shortVideoDescription = this.video.description this.shortVideoDescription = this.video.description
this.completeVideoDescription = description this.completeVideoDescription = description
this.updateVideoDescription(this.completeVideoDescription) this.updateVideoDescription(this.completeVideoDescription)
}, },
error => { error => {
this.descriptionLoading = false this.descriptionLoading = false
this.notificationsService.error(this.i18n('Error'), error.message) this.notificationsService.error(this.i18n('Error'), error.message)
} }
) )
} }
showReportModal (event: Event) { showReportModal (event: Event) {
@ -259,19 +257,19 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
if (res === false) return if (res === false) return
this.videoService.removeVideo(this.video.id) this.videoService.removeVideo(this.video.id)
.subscribe( .subscribe(
status => { status => {
this.notificationsService.success( this.notificationsService.success(
this.i18n('Success'), this.i18n('Success'),
this.i18n('Video {{videoName}} deleted.', { videoName: this.video.name }) this.i18n('Video {{videoName}} deleted.', { videoName: this.video.name })
) )
// Go back to the video-list. // Go back to the video-list.
this.redirectService.redirectToHomepage() this.redirectService.redirectToHomepage()
}, },
error => this.notificationsService.error(this.i18n('Error'), error.message) error => this.notificationsService.error(this.i18n('Error'), error.message)
) )
} }
acceptedPrivacyConcern () { acceptedPrivacyConcern () {
@ -279,6 +277,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.hasAlreadyAcceptedPrivacyConcern = true this.hasAlreadyAcceptedPrivacyConcern = true
} }
isVideoToTranscode () {
return this.video && this.video.state.id === VideoState.TO_TRANSCODE
}
private updateVideoDescription (description: string) { private updateVideoDescription (description: string) {
this.video.description = description this.video.description = description
this.setVideoDescriptionHTML() this.setVideoDescriptionHTML()
@ -294,10 +296,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
} }
private setVideoLikesBarTooltipText () { private setVideoLikesBarTooltipText () {
this.likesBarTooltipText = this.i18n( this.likesBarTooltipText = this.i18n('{{likesNumber}} likes / {{dislikesNumber}} dislikes', {
'{{likesNumber}} likes / {{dislikesNumber}} dislikes', likesNumber: this.video.likes,
{ likesNumber: this.video.likes, dislikesNumber: this.video.dislikes } dislikesNumber: this.video.dislikes
) })
} }
private handleError (err: any) { private handleError (err: any) {
@ -320,15 +322,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
if (this.isUserLoggedIn() === false) return if (this.isUserLoggedIn() === false) return
this.videoService.getUserVideoRating(this.video.id) this.videoService.getUserVideoRating(this.video.id)
.subscribe( .subscribe(
ratingObject => { ratingObject => {
if (ratingObject) { if (ratingObject) {
this.userRating = ratingObject.rating 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) { private async onVideoFetched (video: VideoDetails, startTime = 0) {
@ -409,14 +411,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
} }
method.call(this.videoService, this.video.id) method.call(this.videoService, this.video.id)
.subscribe( .subscribe(
() => { () => {
// Update the video like attribute // Update the video like attribute
this.updateVideoRating(this.userRating, nextRating) this.updateVideoRating(this.userRating, nextRating)
this.userRating = nextRating this.userRating = nextRating
}, },
err => this.notificationsService.error(this.i18n('Error'), err.message)
) err => this.notificationsService.error(this.i18n('Error'), err.message)
)
} }
private updateVideoRating (oldRating: UserVideoRateType, newRating: VideoRateType) { private updateVideoRating (oldRating: UserVideoRateType, newRating: VideoRateType) {

View File

@ -68,7 +68,6 @@
} }
}, },
"lint-staged": { "lint-staged": {
"*.{css,md}": "precise-commits",
"*.scss": [ "*.scss": [
"sass-lint -c .sass-lint.yml", "sass-lint -c .sass-lint.yml",
"git add" "git add"
@ -166,7 +165,6 @@
"maildev": "^1.0.0-rc3", "maildev": "^1.0.0-rc3",
"mocha": "^5.0.0", "mocha": "^5.0.0",
"nodemon": "^1.11.0", "nodemon": "^1.11.0",
"precise-commits": "^1.0.2",
"prettier": "1.13.2", "prettier": "1.13.2",
"prompt": "^1.0.0", "prompt": "^1.0.0",
"sass-lint": "^1.12.1", "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) { async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) {
const video: VideoModel = res.locals.video 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) const videoObject = audiencify(video.toActivityPubObject(), audience)
if (req.path.endsWith('/activity')) { 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) 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 threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined)
const isPublic = true // Comments are always public 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) const videoCommentObject = audiencify(videoComment.toActivityPubObject(threadParentComments), audience)
if (req.path.endsWith('/activity')) { 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) 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 // This is a shared video
if (video.VideoShares !== undefined && video.VideoShares.length !== 0) { if (video.VideoShares !== undefined && video.VideoShares.length !== 0) {
const videoShare = video.VideoShares[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) activities.push(announceActivity)
} else { } else {
const videoObject = video.toActivityPubObject() 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) activities.push(createActivity)
} }

View File

@ -166,7 +166,7 @@ export {
async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) { async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
const user = res.locals.oauth.token.User as UserModel const user = res.locals.oauth.token.User as UserModel
const resultList = await VideoModel.listAccountVideosForApi( const resultList = await VideoModel.listUserVideosForApi(
user.Account.id, user.Account.id,
req.query.start as number, req.query.start as number,
req.query.count 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 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 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) { 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 user = res.locals.oauth.token.user
const actor = user.Account.Actor const actor = user.Account.Actor

View File

@ -1,6 +1,6 @@
import * as express from 'express' import * as express from 'express'
import { extname, join } from 'path' 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 { renamePromise } from '../../../helpers/core-utils'
import { retryTransactionWrapper } from '../../../helpers/database-utils' import { retryTransactionWrapper } from '../../../helpers/database-utils'
import { getVideoFileResolution } from '../../../helpers/ffmpeg-utils' import { getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
@ -21,11 +21,11 @@ import {
} from '../../../initializers' } from '../../../initializers'
import { import {
changeVideoChannelShare, changeVideoChannelShare,
federateVideoIfNeeded,
fetchRemoteVideoDescription, fetchRemoteVideoDescription,
getVideoActivityPubUrl, getVideoActivityPubUrl
shareVideoByServerAndChannel
} from '../../../lib/activitypub' } from '../../../lib/activitypub'
import { sendCreateVideo, sendCreateView, sendUpdateVideo } from '../../../lib/activitypub/send' import { sendCreateView } from '../../../lib/activitypub/send'
import { JobQueue } from '../../../lib/job-queue' import { JobQueue } from '../../../lib/job-queue'
import { Redis } from '../../../lib/redis' import { Redis } from '../../../lib/redis'
import { import {
@ -51,7 +51,7 @@ import { videoCommentRouter } from './comment'
import { rateVideoRouter } from './rate' import { rateVideoRouter } from './rate'
import { VideoFilter } from '../../../../shared/models/videos/video-query.type' import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.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() const videosRouter = express.Router()
@ -185,8 +185,10 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
category: videoInfo.category, category: videoInfo.category,
licence: videoInfo.licence, licence: videoInfo.licence,
language: videoInfo.language, language: videoInfo.language,
commentsEnabled: videoInfo.commentsEnabled, commentsEnabled: videoInfo.commentsEnabled || false,
nsfw: videoInfo.nsfw, waitTranscoding: videoInfo.waitTranscoding || false,
state: CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED,
nsfw: videoInfo.nsfw || false,
description: videoInfo.description, description: videoInfo.description,
support: videoInfo.support, support: videoInfo.support,
privacy: videoInfo.privacy, privacy: videoInfo.privacy,
@ -194,19 +196,20 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
channelId: res.locals.videoChannel.id channelId: res.locals.videoChannel.id
} }
const video = new VideoModel(videoData) 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 { videoFileResolution } = await getVideoFileResolution(videoPhysicalFile.path)
const videoFileData = { const videoFileData = {
extname: extname(videoPhysicalFile.filename), extname: extname(videoPhysicalFile.filename),
resolution: videoFileResolution, resolution: videoFileResolution,
size: videoPhysicalFile.size size: videoPhysicalFile.size
} }
const videoFile = new VideoFileModel(videoFileData) const videoFile = new VideoFileModel(videoFileData)
// Move physical file
const videoDir = CONFIG.STORAGE.VIDEOS_DIR const videoDir = CONFIG.STORAGE.VIDEOS_DIR
const destination = join(videoDir, video.getVideoFilename(videoFile)) const destination = join(videoDir, video.getVideoFilename(videoFile))
await renamePromise(videoPhysicalFile.path, destination) await renamePromise(videoPhysicalFile.path, destination)
// This is important in case if there is another attempt in the retry process // This is important in case if there is another attempt in the retry process
videoPhysicalFile.filename = video.getVideoFilename(videoFile) videoPhysicalFile.filename = video.getVideoFilename(videoFile)
@ -230,6 +233,7 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
await video.createPreview(videoFile) await video.createPreview(videoFile)
} }
// Create the torrent file
await video.createTorrentAndSetInfoHash(videoFile) await video.createTorrentAndSetInfoHash(videoFile)
const videoCreated = await sequelizeTypescript.transaction(async t => { const videoCreated = await sequelizeTypescript.transaction(async t => {
@ -251,20 +255,14 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
video.Tags = tagInstances video.Tags = tagInstances
} }
// Let transcoding job send the video to friends because the video file extension might change await federateVideoIfNeeded(video, true, t)
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)
logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid) logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid)
return videoCreated 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 // Put uuid because we don't have id auto incremented for now
const dataInput = { const dataInput = {
videoUUID: videoCreated.uuid, 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.licence !== undefined) videoInstance.set('licence', videoInfoToUpdate.licence)
if (videoInfoToUpdate.language !== undefined) videoInstance.set('language', videoInfoToUpdate.language) if (videoInfoToUpdate.language !== undefined) videoInstance.set('language', videoInfoToUpdate.language)
if (videoInfoToUpdate.nsfw !== undefined) videoInstance.set('nsfw', videoInfoToUpdate.nsfw) 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.support !== undefined) videoInstance.set('support', videoInfoToUpdate.support)
if (videoInfoToUpdate.description !== undefined) videoInstance.set('description', videoInfoToUpdate.description) if (videoInfoToUpdate.description !== undefined) videoInstance.set('description', videoInfoToUpdate.description)
if (videoInfoToUpdate.commentsEnabled !== undefined) videoInstance.set('commentsEnabled', videoInfoToUpdate.commentsEnabled) 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? // Video channel update?
if (res.locals.videoChannel && videoInstanceUpdated.channelId !== res.locals.videoChannel.id) { if (res.locals.videoChannel && videoInstanceUpdated.channelId !== res.locals.videoChannel.id) {
await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel, { transaction: t }) 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) if (wasPrivateVideo === false) await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t)
} }
// Now we'll update the video's meta data to our friends const isNewVideo = wasPrivateVideo && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE
if (wasPrivateVideo === false) await sendUpdateVideo(videoInstanceUpdated, t) await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo)
// 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)
}
}) })
logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid) 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' import { pageToStartAndCount } from './core-utils'
function activityPubContextify <T> (data: T) { function activityPubContextify <T> (data: T) {
return Object.assign(data,{ return Object.assign(data, {
'@context': [ '@context': [
'https://www.w3.org/ns/activitystreams', 'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1', 'https://w3id.org/security/v1',
{ {
'RsaSignature2017': 'https://w3id.org/security#RsaSignature2017', RsaSignature2017: 'https://w3id.org/security#RsaSignature2017',
'Hashtag': 'as:Hashtag', Hashtag: 'as:Hashtag',
'uuid': 'http://schema.org/identifier', uuid: 'http://schema.org/identifier',
'category': 'http://schema.org/category', category: 'http://schema.org/category',
'licence': 'http://schema.org/license', licence: 'http://schema.org/license',
'sensitive': 'as:sensitive', sensitive: 'as:sensitive',
'language': 'http://schema.org/inLanguage', language: 'http://schema.org/inLanguage',
'views': 'http://schema.org/Number', views: 'http://schema.org/Number',
'size': 'http://schema.org/Number', stats: 'http://schema.org/Number',
'commentsEnabled': 'http://schema.org/Boolean', size: 'http://schema.org/Number',
'support': 'http://schema.org/Text' commentsEnabled: 'http://schema.org/Boolean',
waitTranscoding: 'http://schema.org/Boolean',
support: 'http://schema.org/Text'
}, },
{ {
likes: { likes: {

View File

@ -6,11 +6,13 @@ import {
isVideoAbuseReasonValid, isVideoAbuseReasonValid,
isVideoDurationValid, isVideoDurationValid,
isVideoNameValid, isVideoNameValid,
isVideoStateValid,
isVideoTagValid, isVideoTagValid,
isVideoTruncatedDescriptionValid, isVideoTruncatedDescriptionValid,
isVideoViewsValid isVideoViewsValid
} from '../videos' } from '../videos'
import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
import { VideoState } from '../../../../shared/models/videos'
function sanitizeAndCheckVideoTorrentCreateActivity (activity: any) { function sanitizeAndCheckVideoTorrentCreateActivity (activity: any) {
return isBaseActivityValid(activity, 'Create') && return isBaseActivityValid(activity, 'Create') &&
@ -50,6 +52,10 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
if (!setRemoteVideoTruncatedContent(video)) return false if (!setRemoteVideoTruncatedContent(video)) return false
if (!setValidAttributedTo(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) && return isActivityPubUrlValid(video.id) &&
isVideoNameValid(video.name) && isVideoNameValid(video.name) &&
isActivityPubVideoDurationValid(video.duration) && isActivityPubVideoDurationValid(video.duration) &&

View File

@ -10,7 +10,8 @@ import {
VIDEO_LICENCES, VIDEO_LICENCES,
VIDEO_MIMETYPE_EXT, VIDEO_MIMETYPE_EXT,
VIDEO_PRIVACIES, VIDEO_PRIVACIES,
VIDEO_RATE_TYPES VIDEO_RATE_TYPES,
VIDEO_STATES
} from '../../initializers' } from '../../initializers'
import { VideoModel } from '../../models/video/video' import { VideoModel } from '../../models/video/video'
import { exists, isArray, isFileValid } from './misc' 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 const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES
function isVideoCategoryValid (value: any) { 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) { function isVideoLicenceValid (value: any) {
return value === null || VIDEO_LICENCES[value] !== undefined return value === null || VIDEO_LICENCES[ value ] !== undefined
} }
function isVideoLanguageValid (value: any) { function isVideoLanguageValid (value: any) {
@ -79,20 +84,22 @@ function isVideoRatingTypeValid (value: string) {
const videoFileTypes = Object.keys(VIDEO_MIMETYPE_EXT).map(m => `(${m})`) const videoFileTypes = Object.keys(VIDEO_MIMETYPE_EXT).map(m => `(${m})`)
const videoFileTypesRegex = videoFileTypes.join('|') const videoFileTypesRegex = videoFileTypes.join('|')
function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) {
return isFileValid(files, videoFileTypesRegex, 'videofile') return isFileValid(files, videoFileTypesRegex, 'videofile')
} }
const videoImageTypes = CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME const videoImageTypes = CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME
.map(v => v.replace('.', '')) .map(v => v.replace('.', ''))
.join('|') .join('|')
const videoImageTypesRegex = `image/(${videoImageTypes})` const videoImageTypesRegex = `image/(${videoImageTypes})`
function isVideoImage (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], field: string) { function isVideoImage (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], field: string) {
return isFileValid(files, videoImageTypesRegex, field, true) return isFileValid(files, videoImageTypesRegex, field, true)
} }
function isVideoPrivacyValid (value: string) { function isVideoPrivacyValid (value: string) {
return validator.isInt(value + '') && VIDEO_PRIVACIES[value] !== undefined return validator.isInt(value + '') && VIDEO_PRIVACIES[ value ] !== undefined
} }
function isVideoFileInfoHashValid (value: string) { function isVideoFileInfoHashValid (value: string) {
@ -118,8 +125,8 @@ async function isVideoExist (id: string, res: Response) {
if (!video) { if (!video) {
res.status(404) res.status(404)
.json({ error: 'Video not found' }) .json({ error: 'Video not found' })
.end() .end()
return false return false
} }
@ -169,6 +176,7 @@ export {
isVideoTagsValid, isVideoTagsValid,
isVideoAbuseReasonValid, isVideoAbuseReasonValid,
isVideoFile, isVideoFile,
isVideoStateValid,
isVideoViewsValid, isVideoViewsValid,
isVideoRatingTypeValid, isVideoRatingTypeValid,
isVideoDurationValid, isVideoDurationValid,

View File

@ -1,6 +1,5 @@
import { Model } from 'sequelize-typescript' import { Model } from 'sequelize-typescript'
import * as ipaddr from 'ipaddr.js' import * as ipaddr from 'ipaddr.js'
const isCidr = require('is-cidr')
import { ResultList } from '../../shared' import { ResultList } from '../../shared'
import { VideoResolution } from '../../shared/models/videos' import { VideoResolution } from '../../shared/models/videos'
import { CONFIG } from '../initializers' import { CONFIG } from '../initializers'
@ -10,6 +9,8 @@ import { ApplicationModel } from '../models/application/application'
import { pseudoRandomBytesPromise } from './core-utils' import { pseudoRandomBytesPromise } from './core-utils'
import { logger } from './logger' import { logger } from './logger'
const isCidr = require('is-cidr')
async function generateRandomString (size: number) { async function generateRandomString (size: number) {
const raw = await pseudoRandomBytesPromise(size) const raw = await pseudoRandomBytesPromise(size)
@ -17,22 +18,20 @@ async function generateRandomString (size: number) {
} }
interface FormattableToJSON { 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[] = [] const formattedObjects: U[] = []
objects.forEach(object => { objects.forEach(object => {
formattedObjects.push(object.toFormattedJSON()) formattedObjects.push(object.toFormattedJSON(formattedArg))
}) })
const res: ResultList<U> = { return {
total: objectsTotal, total: objectsTotal,
data: formattedObjects data: formattedObjects
} } as ResultList<U>
return res
} }
async function isSignupAllowed () { async function isSignupAllowed () {
@ -87,16 +86,17 @@ function computeResolutionsToTranscode (videoFileHeight: number) {
const resolutionsEnabled: number[] = [] const resolutionsEnabled: number[] = []
const configResolutions = CONFIG.TRANSCODING.RESOLUTIONS const configResolutions = CONFIG.TRANSCODING.RESOLUTIONS
// Put in the order we want to proceed jobs
const resolutions = [ const resolutions = [
VideoResolution.H_240P,
VideoResolution.H_360P,
VideoResolution.H_480P, VideoResolution.H_480P,
VideoResolution.H_360P,
VideoResolution.H_720P, VideoResolution.H_720P,
VideoResolution.H_240P,
VideoResolution.H_1080P VideoResolution.H_1080P
] ]
for (const resolution of resolutions) { for (const resolution of resolutions) {
if (configResolutions[resolution + 'p'] === true && videoFileHeight > resolution) { if (configResolutions[ resolution + 'p' ] === true && videoFileHeight > resolution) {
resolutionsEnabled.push(resolution) resolutionsEnabled.push(resolution)
} }
} }

View File

@ -1,6 +1,6 @@
import { IConfig } from 'config' import { IConfig } from 'config'
import { dirname, join } from 'path' 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 { ActivityPubActorType } from '../../shared/models/activitypub'
import { FollowState } from '../../shared/models/actors' import { FollowState } from '../../shared/models/actors'
import { VideoPrivacy } from '../../shared/models/videos' 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' [VideoPrivacy.PRIVATE]: 'Private'
} }
const VIDEO_STATES = {
[VideoState.PUBLISHED]: 'Published',
[VideoState.TO_TRANSCODE]: 'To transcode'
}
const VIDEO_MIMETYPE_EXT = { const VIDEO_MIMETYPE_EXT = {
'video/webm': '.webm', 'video/webm': '.webm',
'video/ogg': '.ogv', 'video/ogg': '.ogv',
@ -493,6 +498,7 @@ export {
VIDEO_LANGUAGES, VIDEO_LANGUAGES,
VIDEO_PRIVACIES, VIDEO_PRIVACIES,
VIDEO_LICENCES, VIDEO_LICENCES,
VIDEO_STATES,
VIDEO_RATE_TYPES, VIDEO_RATE_TYPES,
VIDEO_MIMETYPE_EXT, VIDEO_MIMETYPE_EXT,
VIDEO_TRANSCODING_FPS, 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 isOrigin = false
) { ) {
const to = [ ACTIVITY_PUB.PUBLIC ] const to = [ ACTIVITY_PUB.PUBLIC ]
const cc = [ ] const cc = []
// Owner of the video we comment // Owner of the video we comment
if (isOrigin === false) { if (isOrigin === false) {
@ -55,7 +55,7 @@ async function getActorsInvolvedInVideo (video: VideoModel, t: Transaction) {
return actors return actors
} }
async function getAudience (actorSender: ActorModel, t: Transaction, isPublic = true) { function getAudience (actorSender: ActorModel, isPublic = true) {
return buildAudience([ actorSender.followersUrl ], isPublic) return buildAudience([ actorSender.followersUrl ], isPublic)
} }
@ -67,14 +67,14 @@ function buildAudience (followerUrls: string[], isPublic = true) {
to = [ ACTIVITY_PUB.PUBLIC ] to = [ ACTIVITY_PUB.PUBLIC ]
cc = followerUrls cc = followerUrls
} else { // Unlisted } else { // Unlisted
to = [ ] to = []
cc = [ ] cc = []
} }
return { to, cc } return { to, cc }
} }
function audiencify <T> (object: T, audience: ActivityAudience) { function audiencify<T> (object: T, audience: ActivityAudience) {
return Object.assign(object, audience) 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)) { if (Array.isArray(body.orderedItems)) {
const items = 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) await handler(items)
} }

View File

@ -1,7 +1,6 @@
import * as Bluebird from 'bluebird' import * as Bluebird from 'bluebird'
import { ActivityUpdate } from '../../../../shared/models/activitypub' import { ActivityUpdate } from '../../../../shared/models/activitypub'
import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor' import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor'
import { VideoTorrentObject } from '../../../../shared/models/activitypub/objects'
import { retryTransactionWrapper } from '../../../helpers/database-utils' import { retryTransactionWrapper } from '../../../helpers/database-utils'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
import { resetSequelizeInstance } from '../../../helpers/utils' import { resetSequelizeInstance } from '../../../helpers/utils'
@ -13,6 +12,7 @@ import { VideoChannelModel } from '../../../models/video/video-channel'
import { VideoFileModel } from '../../../models/video/video-file' import { VideoFileModel } from '../../../models/video/video-file'
import { fetchAvatarIfExists, getOrCreateActorAndServerAndModel, updateActorAvatarInstance, updateActorInstance } from '../actor' import { fetchAvatarIfExists, getOrCreateActorAndServerAndModel, updateActorAvatarInstance, updateActorInstance } from '../actor'
import { import {
fetchRemoteVideo,
generateThumbnailFromUrl, generateThumbnailFromUrl,
getOrCreateAccountAndVideoAndChannel, getOrCreateAccountAndVideoAndChannel,
getOrCreateVideoChannel, getOrCreateVideoChannel,
@ -51,15 +51,18 @@ function processUpdateVideo (actor: ActorModel, activity: ActivityUpdate) {
} }
async function updateRemoteVideo (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 // Fetch video channel outside the transaction
const newVideoChannelActor = await getOrCreateVideoChannel(videoAttributesToUpdate) const newVideoChannelActor = await getOrCreateVideoChannel(videoObject)
const newVideoChannel = newVideoChannelActor.VideoChannel 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 videoInstance = res.video
let videoFieldsSave: any 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) 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('name', videoData.name)
videoInstance.set('uuid', videoData.uuid) videoInstance.set('uuid', videoData.uuid)
videoInstance.set('url', videoData.url) videoInstance.set('url', videoData.url)
@ -88,6 +91,8 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
videoInstance.set('support', videoData.support) videoInstance.set('support', videoData.support)
videoInstance.set('nsfw', videoData.nsfw) videoInstance.set('nsfw', videoData.nsfw)
videoInstance.set('commentsEnabled', videoData.commentsEnabled) videoInstance.set('commentsEnabled', videoData.commentsEnabled)
videoInstance.set('waitTranscoding', videoData.waitTranscoding)
videoInstance.set('state', videoData.state)
videoInstance.set('duration', videoData.duration) videoInstance.set('duration', videoData.duration)
videoInstance.set('createdAt', videoData.createdAt) videoInstance.set('createdAt', videoData.createdAt)
videoInstance.set('updatedAt', videoData.updatedAt) videoInstance.set('updatedAt', videoData.updatedAt)
@ -98,8 +103,8 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
await videoInstance.save(sequelizeOptions) await videoInstance.save(sequelizeOptions)
// Don't block on request // Don't block on request
generateThumbnailFromUrl(videoInstance, videoAttributesToUpdate.icon) generateThumbnailFromUrl(videoInstance, videoObject.icon)
.catch(err => logger.warn('Cannot generate thumbnail of %s.', videoAttributesToUpdate.id, { err })) .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err }))
// Remove old video files // Remove old video files
const videoFileDestroyTasks: Bluebird<void>[] = [] const videoFileDestroyTasks: Bluebird<void>[] = []
@ -108,16 +113,16 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
} }
await Promise.all(videoFileDestroyTasks) await Promise.all(videoFileDestroyTasks)
const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoInstance, videoAttributesToUpdate) const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoInstance, videoObject)
const tasks = videoFileAttributes.map(f => VideoFileModel.create(f)) const tasks = videoFileAttributes.map(f => VideoFileModel.create(f))
await Promise.all(tasks) 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) const tagInstances = await TagModel.findOrCreateTags(tags, t)
await videoInstance.$set('Tags', tagInstances, sequelizeOptions) 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) { } catch (err) {
if (videoInstance !== undefined && videoFieldsSave !== undefined) { if (videoInstance !== undefined && videoFieldsSave !== undefined) {
resetSequelizeInstance(videoInstance, videoFieldsSave) resetSequelizeInstance(videoInstance, videoFieldsSave)

View File

@ -11,7 +11,7 @@ async function buildVideoAnnounce (byActor: ActorModel, videoShare: VideoShareMo
const accountsToForwardView = await getActorsInvolvedInVideo(video, t) const accountsToForwardView = await getActorsInvolvedInVideo(video, t)
const audience = getObjectFollowersAudience(accountsToForwardView) 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) { 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) return broadcastToFollowers(data, byActor, [ byActor ], t)
} }
async function announceActivityData ( function announceActivityData (url: string, byActor: ActorModel, object: string, audience?: ActivityAudience): ActivityAnnounce {
url: string, if (!audience) audience = getAudience(byActor)
byActor: ActorModel,
object: string,
t: Transaction,
audience?: ActivityAudience
): Promise<ActivityAnnounce> {
if (!audience) {
audience = await getAudience(byActor, t)
}
return { return {
type: 'Announce', type: 'Announce',

View File

@ -23,8 +23,8 @@ async function sendCreateVideo (video: VideoModel, t: Transaction) {
const byActor = video.VideoChannel.Account.Actor const byActor = video.VideoChannel.Account.Actor
const videoObject = video.toActivityPubObject() 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 createActivityData(video.url, byActor, videoObject, t, audience) const data = createActivityData(video.url, byActor, videoObject, audience)
return broadcastToFollowers(data, byActor, [ byActor ], t) return broadcastToFollowers(data, byActor, [ byActor ], t)
} }
@ -33,7 +33,7 @@ async function sendVideoAbuse (byActor: ActorModel, videoAbuse: VideoAbuseModel,
const url = getVideoAbuseActivityPubUrl(videoAbuse) const url = getVideoAbuseActivityPubUrl(videoAbuse)
const audience = { to: [ video.VideoChannel.Account.Actor.url ], cc: [] } 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) 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)) 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 // This was a reply, send it to the parent actors
const actorsException = [ byActor ] const actorsException = [ byActor ]
@ -82,14 +82,14 @@ async function sendCreateView (byActor: ActorModel, video: VideoModel, t: Transa
// Send to origin // Send to origin
if (video.isOwned() === false) { if (video.isOwned() === false) {
const audience = getVideoAudience(video, actorsInvolvedInVideo) 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) return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
} }
// Send to followers // Send to followers
const audience = getObjectFollowersAudience(actorsInvolvedInVideo) 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 // Use the server actor to send the view
const serverActor = await getServerActor() const serverActor = await getServerActor()
@ -106,34 +106,31 @@ async function sendCreateDislike (byActor: ActorModel, video: VideoModel, t: Tra
// Send to origin // Send to origin
if (video.isOwned() === false) { if (video.isOwned() === false) {
const audience = getVideoAudience(video, actorsInvolvedInVideo) 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) return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
} }
// Send to followers // Send to followers
const audience = getObjectFollowersAudience(actorsInvolvedInVideo) const audience = getObjectFollowersAudience(actorsInvolvedInVideo)
const data = await createActivityData(url, byActor, dislikeActivityData, t, audience) const data = createActivityData(url, byActor, dislikeActivityData, audience)
const actorsException = [ byActor ] const actorsException = [ byActor ]
return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, actorsException) return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, actorsException)
} }
async function createActivityData (url: string, function createActivityData (url: string, byActor: ActorModel, object: any, audience?: ActivityAudience): ActivityCreate {
byActor: ActorModel, if (!audience) audience = getAudience(byActor)
object: any,
t: Transaction,
audience?: ActivityAudience): Promise<ActivityCreate> {
if (!audience) {
audience = await getAudience(byActor, t)
}
return audiencify({ return audiencify(
type: 'Create' as 'Create', {
id: url + '/activity', type: 'Create' as 'Create',
actor: byActor.url, id: url + '/activity',
object: audiencify(object, audience) actor: byActor.url,
}, audience) object: audiencify(object, audience)
},
audience
)
} }
function createDislikeActivityData (byActor: ActorModel, video: VideoModel) { function createDislikeActivityData (byActor: ActorModel, video: VideoModel) {

View File

@ -14,36 +14,31 @@ async function sendLike (byActor: ActorModel, video: VideoModel, t: Transaction)
// Send to origin // Send to origin
if (video.isOwned() === false) { if (video.isOwned() === false) {
const audience = getVideoAudience(video, accountsInvolvedInVideo) 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) return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
} }
// Send to followers // Send to followers
const audience = getObjectFollowersAudience(accountsInvolvedInVideo) const audience = getObjectFollowersAudience(accountsInvolvedInVideo)
const data = await likeActivityData(url, byActor, video, t, audience) const data = likeActivityData(url, byActor, video, audience)
const followersException = [ byActor ] const followersException = [ byActor ]
return broadcastToFollowers(data, byActor, accountsInvolvedInVideo, t, followersException) return broadcastToFollowers(data, byActor, accountsInvolvedInVideo, t, followersException)
} }
async function likeActivityData ( function likeActivityData (url: string, byActor: ActorModel, video: VideoModel, audience?: ActivityAudience): ActivityLike {
url: string, if (!audience) audience = getAudience(byActor)
byActor: ActorModel,
video: VideoModel,
t: Transaction,
audience?: ActivityAudience
): Promise<ActivityLike> {
if (!audience) {
audience = await getAudience(byActor, t)
}
return audiencify({ return audiencify(
type: 'Like' as 'Like', {
id: url, type: 'Like' as 'Like',
actor: byActor.url, id: url,
object: video.url actor: byActor.url,
}, audience) object: video.url
},
audience
)
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -27,7 +27,7 @@ async function sendUndoFollow (actorFollow: ActorFollowModel, t: Transaction) {
const undoUrl = getUndoActivityPubUrl(followUrl) const undoUrl = getUndoActivityPubUrl(followUrl)
const object = followActivityData(followUrl, me, following) 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) return unicastTo(data, me, following.inboxUrl)
} }
@ -37,18 +37,18 @@ async function sendUndoLike (byActor: ActorModel, video: VideoModel, t: Transact
const undoUrl = getUndoActivityPubUrl(likeUrl) const undoUrl = getUndoActivityPubUrl(likeUrl)
const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t) const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
const object = await likeActivityData(likeUrl, byActor, video, t) const object = likeActivityData(likeUrl, byActor, video)
// Send to origin // Send to origin
if (video.isOwned() === false) { if (video.isOwned() === false) {
const audience = getVideoAudience(video, actorsInvolvedInVideo) 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) return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
} }
const audience = getObjectFollowersAudience(actorsInvolvedInVideo) const audience = getObjectFollowersAudience(actorsInvolvedInVideo)
const data = await undoActivityData(undoUrl, byActor, object, t, audience) const data = undoActivityData(undoUrl, byActor, object, audience)
const followersException = [ byActor ] const followersException = [ byActor ]
return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException) 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 actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
const dislikeActivity = createDislikeActivityData(byActor, video) const dislikeActivity = createDislikeActivityData(byActor, video)
const object = await createActivityData(dislikeUrl, byActor, dislikeActivity, t) const object = createActivityData(dislikeUrl, byActor, dislikeActivity)
if (video.isOwned() === false) { if (video.isOwned() === false) {
const audience = getVideoAudience(video, actorsInvolvedInVideo) 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) 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 ] const followersException = [ byActor ]
return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException) 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 actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
const object = await buildVideoAnnounce(byActor, videoShare, 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 ] const followersException = [ byActor ]
return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException) return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException)
@ -97,21 +97,21 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function undoActivityData ( function undoActivityData (
url: string, url: string,
byActor: ActorModel, byActor: ActorModel,
object: ActivityFollow | ActivityLike | ActivityCreate | ActivityAnnounce, object: ActivityFollow | ActivityLike | ActivityCreate | ActivityAnnounce,
t: Transaction,
audience?: ActivityAudience audience?: ActivityAudience
): Promise<ActivityUndo> { ): ActivityUndo {
if (!audience) { if (!audience) audience = getAudience(byActor)
audience = await getAudience(byActor, t)
}
return audiencify({ return audiencify(
type: 'Undo' as 'Undo', {
id: url, type: 'Undo' as 'Undo',
actor: byActor.url, id: url,
object actor: byActor.url,
}, audience) object
},
audience
)
} }

View File

@ -15,9 +15,9 @@ async function sendUpdateVideo (video: VideoModel, t: Transaction) {
const url = getUpdateActivityPubUrl(video.url, video.updatedAt.toISOString()) const url = getUpdateActivityPubUrl(video.url, video.updatedAt.toISOString())
const videoObject = video.toActivityPubObject() 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) const actorsInvolved = await VideoShareModel.loadActorsByShare(video.id, t)
actorsInvolved.push(byActor) actorsInvolved.push(byActor)
@ -30,8 +30,8 @@ async function sendUpdateActor (accountOrChannel: AccountModel | VideoChannelMod
const url = getUpdateActivityPubUrl(byActor.url, byActor.updatedAt.toISOString()) const url = getUpdateActivityPubUrl(byActor.url, byActor.updatedAt.toISOString())
const accountOrChannelObject = accountOrChannel.toActivityPubObject() const accountOrChannelObject = accountOrChannel.toActivityPubObject()
const audience = await getAudience(byActor, t) const audience = getAudience(byActor)
const data = await updateActivityData(url, byActor, accountOrChannelObject, t, audience) const data = updateActivityData(url, byActor, accountOrChannelObject, audience)
let actorsInvolved: ActorModel[] let actorsInvolved: ActorModel[]
if (accountOrChannel instanceof AccountModel) { if (accountOrChannel instanceof AccountModel) {
@ -56,21 +56,17 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function updateActivityData ( function updateActivityData (url: string, byActor: ActorModel, object: any, audience?: ActivityAudience): ActivityUpdate {
url: string, if (!audience) audience = getAudience(byActor)
byActor: ActorModel,
object: any,
t: Transaction,
audience?: ActivityAudience
): Promise<ActivityUpdate> {
if (!audience) {
audience = await getAudience(byActor, t)
}
return audiencify({ return audiencify(
type: 'Update' as 'Update', {
id: url, type: 'Update' as 'Update',
actor: byActor.url, id: url,
object: audiencify(object, audience) actor: byActor.url,
}, audience) object: audiencify(object, audience
)
},
audience
)
} }

View File

@ -1,8 +1,9 @@
import * as Bluebird from 'bluebird' import * as Bluebird from 'bluebird'
import * as sequelize from 'sequelize'
import * as magnetUtil from 'magnet-uri' import * as magnetUtil from 'magnet-uri'
import { join } from 'path' import { join } from 'path'
import * as request from 'request' import * as request from 'request'
import { ActivityIconObject } from '../../../shared/index' import { ActivityIconObject, VideoState } from '../../../shared/index'
import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos' import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos'
import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/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 { getOrCreateActorAndServerAndModel } from './actor'
import { addVideoComments } from './video-comments' import { addVideoComments } from './video-comments'
import { crawlCollectionPage } from './crawl' 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) { function fetchRemoteVideoPreview (video: VideoModel, reject: Function) {
const host = video.VideoChannel.Account.Actor.Server.host const host = video.VideoChannel.Account.Actor.Server.host
@ -55,9 +71,11 @@ function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject)
return doRequestAndSaveToFile(options, thumbnailPath) return doRequestAndSaveToFile(options, thumbnailPath)
} }
async function videoActivityObjectToDBAttributes (videoChannel: VideoChannelModel, async function videoActivityObjectToDBAttributes (
videoObject: VideoTorrentObject, videoChannel: VideoChannelModel,
to: string[] = []) { videoObject: VideoTorrentObject,
to: string[] = []
) {
const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPrivacy.PUBLIC : VideoPrivacy.UNLISTED const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPrivacy.PUBLIC : VideoPrivacy.UNLISTED
const duration = videoObject.duration.replace(/[^\d]+/, '') const duration = videoObject.duration.replace(/[^\d]+/, '')
@ -90,6 +108,8 @@ async function videoActivityObjectToDBAttributes (videoChannel: VideoChannelMode
support, support,
nsfw: videoObject.sensitive, nsfw: videoObject.sensitive,
commentsEnabled: videoObject.commentsEnabled, commentsEnabled: videoObject.commentsEnabled,
waitTranscoding: videoObject.waitTranscoding,
state: videoObject.state,
channelId: videoChannel.id, channelId: videoChannel.id,
duration: parseInt(duration, 10), duration: parseInt(duration, 10),
createdAt: new Date(videoObject.published), createdAt: new Date(videoObject.published),
@ -185,22 +205,20 @@ async function getOrCreateVideo (videoObject: VideoTorrentObject, channelActor:
} }
async function getOrCreateAccountAndVideoAndChannel (videoObject: VideoTorrentObject | string, actor?: ActorModel) { async function getOrCreateAccountAndVideoAndChannel (videoObject: VideoTorrentObject | string, actor?: ActorModel) {
if (typeof videoObject === 'string') { const videoUrl = typeof videoObject === 'string' ? videoObject : videoObject.id
const videoUrl = videoObject
const videoFromDatabase = await VideoModel.loadByUrlAndPopulateAccount(videoUrl) const videoFromDatabase = await VideoModel.loadByUrlAndPopulateAccount(videoUrl)
if (videoFromDatabase) { if (videoFromDatabase) {
return { return {
video: videoFromDatabase, video: videoFromDatabase,
actor: videoFromDatabase.VideoChannel.Account.Actor, actor: videoFromDatabase.VideoChannel.Account.Actor,
channelActor: videoFromDatabase.VideoChannel.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) { if (!actor) {
const actorObj = videoObject.attributedTo.find(a => a.type === 'Person') const actorObj = videoObject.attributedTo.find(a => a.type === 'Person')
if (!actorObj) throw new Error('Cannot find associated actor to video ' + videoObject.url) 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> { async function fetchRemoteVideo (videoUrl: string): Promise<VideoTorrentObject> {
const options = { const options = {
uri: videoUrl, uri: videoUrl,
@ -324,3 +328,17 @@ async function fetchRemoteVideo (videoUrl: string): Promise<VideoTorrentObject>
return body 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 * as kue from 'kue'
import { VideoResolution } from '../../../../shared' import { VideoResolution, VideoState } from '../../../../shared'
import { VideoPrivacy } from '../../../../shared/models/videos'
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
import { computeResolutionsToTranscode } from '../../../helpers/utils' import { computeResolutionsToTranscode } from '../../../helpers/utils'
import { sequelizeTypescript } from '../../../initializers'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { shareVideoByServerAndChannel } from '../../activitypub'
import { sendCreateVideo, sendUpdateVideo } from '../../activitypub/send'
import { JobQueue } from '../job-queue' import { JobQueue } from '../job-queue'
import { federateVideoIfNeeded } from '../../activitypub'
import { retryTransactionWrapper } from '../../../helpers/database-utils'
import { sequelizeTypescript } from '../../../initializers'
export type VideoFilePayload = { export type VideoFilePayload = {
videoUUID: string videoUUID: string
isNewVideo: boolean isNewVideo?: boolean
resolution?: VideoResolution resolution?: VideoResolution
isPortraitMode?: boolean isPortraitMode?: boolean
} }
@ -52,10 +51,20 @@ async function processVideoFile (job: kue.Job) {
// Transcoding in other resolution // Transcoding in other resolution
if (payload.resolution) { if (payload.resolution) {
await video.transcodeOriginalVideofile(payload.resolution, payload.isPortraitMode) 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 { } else {
await video.optimizeOriginalVideofile() 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 return video
@ -64,68 +73,70 @@ async function processVideoFile (job: kue.Job) {
async function onVideoFileTranscoderOrImportSuccess (video: VideoModel) { async function onVideoFileTranscoderOrImportSuccess (video: VideoModel) {
if (video === undefined) return undefined if (video === undefined) return undefined
// Maybe the video changed in database, refresh it return sequelizeTypescript.transaction(async t => {
const videoDatabase = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(video.uuid) // Maybe the video changed in database, refresh it
// Video does not exist anymore let videoDatabase = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(video.uuid, t)
if (!videoDatabase) return undefined // Video does not exist anymore
if (!videoDatabase) return undefined
if (video.privacy !== VideoPrivacy.PRIVATE) { // We transcoded the video file in another format, now we can publish it
await sendUpdateVideo(video, undefined) 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) { async function onVideoFileOptimizerSuccess (video: VideoModel, isNewVideo: boolean) {
if (video === undefined) return undefined if (video === undefined) return undefined
// Maybe the video changed in database, refresh it // Outside the transaction (IO on disk)
const videoDatabase = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(video.uuid) const { videoFileResolution } = await video.getOriginalFileResolution()
// Video does not exist anymore
if (!videoDatabase) return undefined
if (video.privacy !== VideoPrivacy.PRIVATE) { return sequelizeTypescript.transaction(async t => {
if (isNewVideo !== false) { // Maybe the video changed in database, refresh it
// Now we'll add the video's meta data to our followers const videoDatabase = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(video.uuid, t)
await sequelizeTypescript.transaction(async t => { // Video does not exist anymore
await sendCreateVideo(video, t) if (!videoDatabase) return undefined
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 }
)
// Create transcoding jobs if there are enabled resolutions if (resolutionsEnabled.length !== 0) {
const resolutionsEnabled = computeResolutionsToTranscode(videoFileResolution) const tasks: Promise<any>[] = []
logger.info(
'Resolutions computed for video %s and origin file height of %d.', videoDatabase.uuid, videoFileResolution,
{ resolutions: resolutionsEnabled }
)
if (resolutionsEnabled.length !== 0) { for (const resolution of resolutionsEnabled) {
const tasks: Promise<any>[] = [] const dataInput = {
videoUUID: videoDatabase.uuid,
resolution
}
for (const resolution of resolutionsEnabled) { const p = JobQueue.Instance.createJob({ type: 'video-file', payload: dataInput })
const dataInput = { tasks.push(p)
videoUUID: videoDatabase.uuid,
resolution,
isNewVideo
} }
const p = JobQueue.Instance.createJob({ type: 'video-file', payload: dataInput }) await Promise.all(tasks)
tasks.push(p)
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) return federateVideoIfNeeded(video, isNewVideo, t)
})
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
}
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -79,6 +79,7 @@ class JobQueue {
const res = await handlers[ handlerName ](job) const res = await handlers[ handlerName ](job)
return done(null, res) return done(null, res)
} catch (err) { } catch (err) {
logger.error('Cannot execute job %d.', job.id, { err })
return done(err) return done(err)
} }
}) })

View File

@ -14,7 +14,7 @@ function cacheRoute (lifetime: number) {
// Not cached // Not cached
if (!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) const sendSave = res.send.bind(res)

View File

@ -55,8 +55,13 @@ const videosAddValidator = [
.customSanitizer(toValueOrNull) .customSanitizer(toValueOrNull)
.custom(isVideoLanguageValid).withMessage('Should have a valid language'), .custom(isVideoLanguageValid).withMessage('Should have a valid language'),
body('nsfw') body('nsfw')
.optional()
.toBoolean() .toBoolean()
.custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'), .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') body('description')
.optional() .optional()
.customSanitizer(toValueOrNull) .customSanitizer(toValueOrNull)
@ -70,6 +75,7 @@ const videosAddValidator = [
.customSanitizer(toValueOrNull) .customSanitizer(toValueOrNull)
.custom(isVideoTagsValid).withMessage('Should have correct tags'), .custom(isVideoTagsValid).withMessage('Should have correct tags'),
body('commentsEnabled') body('commentsEnabled')
.optional()
.toBoolean() .toBoolean()
.custom(isBooleanValid).withMessage('Should have comments enabled boolean'), .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
body('privacy') body('privacy')
@ -149,6 +155,10 @@ const videosUpdateValidator = [
.optional() .optional()
.toBoolean() .toBoolean()
.custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'), .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') body('privacy')
.optional() .optional()
.toInt() .toInt()

View File

@ -25,7 +25,7 @@ import {
Table, Table,
UpdatedAt UpdatedAt
} from 'sequelize-typescript' } from 'sequelize-typescript'
import { VideoPrivacy, VideoResolution } from '../../../shared' import { VideoPrivacy, VideoResolution, VideoState } from '../../../shared'
import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
import { VideoFilter } from '../../../shared/models/videos/video-query.type' import { VideoFilter } from '../../../shared/models/videos/video-query.type'
@ -47,7 +47,7 @@ import {
isVideoLanguageValid, isVideoLanguageValid,
isVideoLicenceValid, isVideoLicenceValid,
isVideoNameValid, isVideoNameValid,
isVideoPrivacyValid, isVideoPrivacyValid, isVideoStateValid,
isVideoSupportValid isVideoSupportValid
} from '../../helpers/custom-validators/videos' } from '../../helpers/custom-validators/videos'
import { generateImageFromVideoFile, getVideoFileResolution, transcode } from '../../helpers/ffmpeg-utils' import { generateImageFromVideoFile, getVideoFileResolution, transcode } from '../../helpers/ffmpeg-utils'
@ -66,7 +66,7 @@ import {
VIDEO_EXT_MIMETYPE, VIDEO_EXT_MIMETYPE,
VIDEO_LANGUAGES, VIDEO_LANGUAGES,
VIDEO_LICENCES, VIDEO_LICENCES,
VIDEO_PRIVACIES VIDEO_PRIVACIES, VIDEO_STATES
} from '../../initializers' } from '../../initializers'
import { import {
getVideoCommentsActivityPubUrl, getVideoCommentsActivityPubUrl,
@ -93,10 +93,7 @@ enum ScopeNames {
AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS', WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS',
WITH_TAGS = 'WITH_TAGS', WITH_TAGS = 'WITH_TAGS',
WITH_FILES = 'WITH_FILES', WITH_FILES = 'WITH_FILES'
WITH_SHARES = 'WITH_SHARES',
WITH_RATES = 'WITH_RATES',
WITH_COMMENTS = 'WITH_COMMENTS'
} }
@Scopes({ @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 ] include: [ videoChannelInclude ]
} }
@ -272,42 +282,6 @@ enum ScopeNames {
required: true 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({ @Table({
@ -335,7 +309,7 @@ enum ScopeNames {
fields: [ 'channelId' ] fields: [ 'channelId' ]
}, },
{ {
fields: [ 'id', 'privacy' ] fields: [ 'id', 'privacy', 'state', 'waitTranscoding' ]
}, },
{ {
fields: [ 'url'], fields: [ 'url'],
@ -435,6 +409,16 @@ export class VideoModel extends Model<VideoModel> {
@Column @Column
commentsEnabled: boolean commentsEnabled: boolean
@AllowNull(false)
@Column
waitTranscoding: boolean
@AllowNull(false)
@Default(null)
@Is('VideoState', value => throwIfNotValid(value, isVideoStateValid, 'state'))
@Column
state: VideoState
@CreatedAt @CreatedAt
createdAt: Date 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> = { const query: IFindOptions<VideoModel> = {
offset: start, offset: start,
limit: count, limit: count,
@ -858,12 +842,13 @@ export class VideoModel extends Model<VideoModel> {
.findOne(options) .findOne(options)
} }
static loadByUUIDAndPopulateAccountAndServerAndTags (uuid: string) { static loadByUUIDAndPopulateAccountAndServerAndTags (uuid: string, t?: Sequelize.Transaction) {
const options = { const options = {
order: [ [ 'Tags', 'name', 'ASC' ] ], order: [ [ 'Tags', 'name', 'ASC' ] ],
where: { where: {
uuid uuid
} },
transaction: t
} }
return VideoModel return VideoModel
@ -905,31 +890,23 @@ export class VideoModel extends Model<VideoModel> {
} }
private static getCategoryLabel (id: number) { private static getCategoryLabel (id: number) {
let categoryLabel = VIDEO_CATEGORIES[id] return VIDEO_CATEGORIES[id] || 'Misc'
if (!categoryLabel) categoryLabel = 'Misc'
return categoryLabel
} }
private static getLicenceLabel (id: number) { private static getLicenceLabel (id: number) {
let licenceLabel = VIDEO_LICENCES[id] return VIDEO_LICENCES[id] || 'Unknown'
if (!licenceLabel) licenceLabel = 'Unknown'
return licenceLabel
} }
private static getLanguageLabel (id: string) { private static getLanguageLabel (id: string) {
let languageLabel = VIDEO_LANGUAGES[id] return VIDEO_LANGUAGES[id] || 'Unknown'
if (!languageLabel) languageLabel = 'Unknown'
return languageLabel
} }
private static getPrivacyLabel (id: number) { private static getPrivacyLabel (id: number) {
let privacyLabel = VIDEO_PRIVACIES[id] return VIDEO_PRIVACIES[id] || 'Unknown'
if (!privacyLabel) privacyLabel = 'Unknown' }
return privacyLabel private static getStateLabel (id: number) {
return VIDEO_STATES[id] || 'Unknown'
} }
getOriginalFile () { getOriginalFile () {
@ -1026,11 +1003,16 @@ export class VideoModel extends Model<VideoModel> {
return join(STATIC_PATHS.PREVIEWS, this.getPreviewName()) return join(STATIC_PATHS.PREVIEWS, this.getPreviewName())
} }
toFormattedJSON (): Video { toFormattedJSON (options?: {
additionalAttributes: {
state: boolean,
waitTranscoding: boolean
}
}): Video {
const formattedAccount = this.VideoChannel.Account.toFormattedJSON() const formattedAccount = this.VideoChannel.Account.toFormattedJSON()
const formattedVideoChannel = this.VideoChannel.toFormattedJSON() const formattedVideoChannel = this.VideoChannel.toFormattedJSON()
return { const videoObject: Video = {
id: this.id, id: this.id,
uuid: this.uuid, uuid: this.uuid,
name: this.name, name: this.name,
@ -1082,6 +1064,19 @@ export class VideoModel extends Model<VideoModel> {
avatar: formattedVideoChannel.avatar 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 { toFormattedDetailsJSON (): VideoDetails {
@ -1094,6 +1089,11 @@ export class VideoModel extends Model<VideoModel> {
account: this.VideoChannel.Account.toFormattedJSON(), account: this.VideoChannel.Account.toFormattedJSON(),
tags: map(this.Tags, 'name'), tags: map(this.Tags, 'name'),
commentsEnabled: this.commentsEnabled, commentsEnabled: this.commentsEnabled,
waitTranscoding: this.waitTranscoding,
state: {
id: this.state,
label: VideoModel.getStateLabel(this.state)
},
files: [] files: []
} }
@ -1207,6 +1207,8 @@ export class VideoModel extends Model<VideoModel> {
language, language,
views: this.views, views: this.views,
sensitive: this.nsfw, sensitive: this.nsfw,
waitTranscoding: this.waitTranscoding,
state: this.state,
commentsEnabled: this.commentsEnabled, commentsEnabled: this.commentsEnabled,
published: this.publishedAt.toISOString(), published: this.publishedAt.toISOString(),
updated: this.updatedAt.toISOString(), updated: this.updatedAt.toISOString(),

View File

@ -175,6 +175,7 @@ describe('Test videos API validator', function () {
language: 'pt', language: 'pt',
nsfw: false, nsfw: false,
commentsEnabled: true, commentsEnabled: true,
waitTranscoding: true,
description: 'my super description', description: 'my super description',
support: 'my super support text', support: 'my super support text',
tags: [ 'tag1', 'tag2' ], 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 }) 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 () { it('Should fail with a long description', async function () {
const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(2500) }) const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(2500) })
const attaches = baseCorrectAttaches const attaches = baseCorrectAttaches

View File

@ -924,7 +924,7 @@ describe('Test multiple servers', function () {
describe('With minimum parameters', function () { describe('With minimum parameters', function () {
it('Should upload and propagate the video', async function () { it('Should upload and propagate the video', async function () {
this.timeout(50000) this.timeout(60000)
const path = '/api/v1/videos/upload' const path = '/api/v1/videos/upload'
@ -934,16 +934,14 @@ describe('Test multiple servers', function () {
.set('Authorization', 'Bearer ' + servers[1].accessToken) .set('Authorization', 'Bearer ' + servers[1].accessToken)
.field('name', 'minimum parameters') .field('name', 'minimum parameters')
.field('privacy', '1') .field('privacy', '1')
.field('nsfw', 'false')
.field('channelId', '1') .field('channelId', '1')
.field('commentsEnabled', 'true')
const filePath = join(__dirname, '..', '..', 'fixtures', 'video_short.webm') const filePath = join(__dirname, '..', '..', 'fixtures', 'video_short.webm')
await req.attach('videofile', filePath) await req.attach('videofile', filePath)
.expect(200) .expect(200)
await wait(25000) await wait(40000)
for (const server of servers) { for (const server of servers) {
const res = await getVideosList(server.url) const res = await getVideosList(server.url)
@ -964,7 +962,7 @@ describe('Test multiple servers', function () {
}, },
isLocal, isLocal,
duration: 5, duration: 5,
commentsEnabled: true, commentsEnabled: false,
tags: [ ], tags: [ ],
privacy: VideoPrivacy.PUBLIC, privacy: VideoPrivacy.PUBLIC,
channel: { channel: {

View File

@ -32,7 +32,8 @@ describe('Test services', function () {
const oembedUrl = 'http://localhost:9001/videos/watch/' + server.video.uuid const oembedUrl = 'http://localhost:9001/videos/watch/' + server.video.uuid
const res = await getOEmbed(server.url, oembedUrl) 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>' 'frameborder="0" allowfullscreen></iframe>'
const expectedThumbnailUrl = 'http://localhost:9001/static/previews/' + server.video.uuid + '.jpg' const expectedThumbnailUrl = 'http://localhost:9001/static/previews/' + server.video.uuid + '.jpg'

View File

@ -2,11 +2,22 @@
import * as chai from 'chai' import * as chai from 'chai'
import 'mocha' import 'mocha'
import { VideoDetails } from '../../../../shared/models/videos' import { VideoDetails, VideoState } from '../../../../shared/models/videos'
import { getVideoFileFPS } from '../../../helpers/ffmpeg-utils' import { getVideoFileFPS } from '../../../helpers/ffmpeg-utils'
import { import {
flushAndRunMultipleServers, flushTests, getVideo, getVideosList, killallServers, root, ServerInfo, setAccessTokensToServers, uploadVideo, doubleFollow,
wait, webtorrentAdd flushAndRunMultipleServers,
flushTests,
getMyVideos,
getVideo,
getVideosList,
killallServers,
root,
ServerInfo,
setAccessTokensToServers,
uploadVideo,
wait,
webtorrentAdd
} from '../../utils' } from '../../utils'
import { join } from 'path' 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 () { after(async function () {
killallServers(servers) killallServers(servers)

View File

@ -65,7 +65,7 @@ describe('Test create transcoding jobs', function () {
const env = getEnvCli(servers[0]) const env = getEnvCli(servers[0])
await execCLI(`${env} npm run create-transcoding-job -- -v ${video2UUID}`) await execCLI(`${env} npm run create-transcoding-job -- -v ${video2UUID}`)
await wait(30000) await wait(40000)
for (const server of servers) { for (const server of servers) {
const res = await getVideosList(server.url) const res = await getVideosList(server.url)

View File

@ -27,6 +27,7 @@ type VideoAttributes = {
language?: string language?: string
nsfw?: boolean nsfw?: boolean
commentsEnabled?: boolean commentsEnabled?: boolean
waitTranscoding?: boolean
description?: string description?: string
tags?: string[] tags?: string[]
channelId?: number channelId?: number
@ -326,6 +327,7 @@ async function uploadVideo (url: string, accessToken: string, videoAttributesArg
language: 'zh', language: 'zh',
channelId: defaultChannelId, channelId: defaultChannelId,
nsfw: true, nsfw: true,
waitTranscoding: false,
description: 'my super description', description: 'my super description',
support: 'my super support text', support: 'my super support text',
tags: [ 'tag' ], tags: [ 'tag' ],
@ -341,6 +343,7 @@ async function uploadVideo (url: string, accessToken: string, videoAttributesArg
.field('name', attributes.name) .field('name', attributes.name)
.field('nsfw', JSON.stringify(attributes.nsfw)) .field('nsfw', JSON.stringify(attributes.nsfw))
.field('commentsEnabled', JSON.stringify(attributes.commentsEnabled)) .field('commentsEnabled', JSON.stringify(attributes.commentsEnabled))
.field('waitTranscoding', JSON.stringify(attributes.waitTranscoding))
.field('privacy', attributes.privacy.toString()) .field('privacy', attributes.privacy.toString())
.field('channelId', attributes.channelId) .field('channelId', attributes.channelId)

View File

@ -176,6 +176,7 @@ async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, languag
licence, licence,
language, language,
nsfw: isNSFW(videoInfo), nsfw: isNSFW(videoInfo),
waitTranscoding: true,
commentsEnabled: true, commentsEnabled: true,
description: videoInfo.description || undefined, description: videoInfo.description || undefined,
support: undefined, support: undefined,

View File

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

View File

@ -5,6 +5,7 @@ import {
ActivityUrlObject ActivityUrlObject
} from './common-objects' } from './common-objects'
import { ActivityPubOrderedCollection } from '../activitypub-ordered-collection' import { ActivityPubOrderedCollection } from '../activitypub-ordered-collection'
import { VideoState } from '../../videos'
export interface VideoTorrentObject { export interface VideoTorrentObject {
type: 'Video' type: 'Video'
@ -19,6 +20,8 @@ export interface VideoTorrentObject {
views: number views: number
sensitive: boolean sensitive: boolean
commentsEnabled: boolean commentsEnabled: boolean
waitTranscoding: boolean
state: VideoState
published: string published: string
updated: string updated: string
mediaType: 'text/markdown' mediaType: 'text/markdown'

View File

@ -13,3 +13,4 @@ export * from './video-rate.type'
export * from './video-resolution.enum' export * from './video-resolution.enum'
export * from './video-update.model' export * from './video-update.model'
export * from './video.model' export * from './video.model'
export * from './video-state.enum'

View File

@ -7,7 +7,8 @@ export interface VideoCreate {
description?: string description?: string
support?: string support?: string
channelId: number channelId: number
nsfw: boolean nsfw?: boolean
waitTranscoding?: boolean
name: string name: string
tags?: string[] tags?: string[]
commentsEnabled?: boolean 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[] tags?: string[]
commentsEnabled?: boolean commentsEnabled?: boolean
nsfw?: boolean nsfw?: boolean
waitTranscoding?: boolean
channelId?: number channelId?: number
thumbnailfile?: Blob thumbnailfile?: Blob
previewfile?: Blob previewfile?: Blob

View File

@ -1,4 +1,4 @@
import { VideoResolution } from '../../index' import { VideoResolution, VideoState } from '../../index'
import { Account } from '../actors' import { Account } from '../actors'
import { Avatar } from '../avatars/avatar.model' import { Avatar } from '../avatars/avatar.model'
import { VideoChannel } from './video-channel.model' import { VideoChannel } from './video-channel.model'
@ -41,6 +41,9 @@ export interface Video {
dislikes: number dislikes: number
nsfw: boolean nsfw: boolean
waitTranscoding?: boolean
state?: VideoConstant<VideoState>
account: { account: {
id: number id: number
uuid: string uuid: string
@ -70,4 +73,8 @@ export interface VideoDetails extends Video {
files: VideoFile[] files: VideoFile[]
account: Account account: Account
commentsEnabled: boolean commentsEnabled: boolean
// Not optional in details (unlike in Video)
waitTranscoding: boolean
state: VideoConstant<VideoState>
} }

View File

@ -3435,6 +3435,19 @@
<p>Video description</p> <p>Video description</p>
</div> </div>
</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-row prop-group">
<div class="prop-name"> <div class="prop-name">
<div class="prop-title">support</div> <div class="prop-title">support</div>
@ -4009,6 +4022,19 @@
<p>Video category</p> <p>Video category</p>
</div> </div>
</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-row prop-group">
<div class="prop-name"> <div class="prop-name">
<div class="prop-title">licence</div> <div class="prop-title">licence</div>

View File

@ -682,6 +682,10 @@ paths:
in: formData in: formData
type: string type: string
description: 'Video description' description: 'Video description'
- name: waitTranscoding
in: formData
type: boolean
description: 'Whether or not we wait transcoding before publish the video'
- name: support - name: support
in: formData in: formData
type: string type: string
@ -814,6 +818,10 @@ paths:
in: formData in: formData
type: number type: number
description: 'Video category' description: 'Video category'
- name: waitTranscoding
in: formData
type: boolean
description: 'Whether or not we wait transcoding before publish the video'
- name: licence - name: licence
in: formData in: formData
type: number type: number

View File

@ -63,13 +63,18 @@ $ node dist/server/tools/import-videos.js \
* Vimeo: https://vimeo.com/xxxxxx * Vimeo: https://vimeo.com/xxxxxx
* Dailymotion: https://www.dailymotion.com/xxxxx * Dailymotion: https://www.dailymotion.com/xxxxx
The script will get all public videos from Youtube, download them and upload to PeerTube. 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... 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 ### upload.js
You can use this script to import videos directly from the CLI. 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} $ cd ${CLONE}
$ node dist/server/tools/upload.js --help $ node dist/server/tools/upload.js --help