Add concept of video state, and add ability to wait transcoding before
publishing a video
This commit is contained in:
parent
6ccdf3a23e
commit
2186386cca
|
@ -18,7 +18,7 @@
|
|||
<div class="video-info">
|
||||
<a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
|
||||
<span i18n class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>
|
||||
<div class="video-info-private">{{ video.privacy.label }}</div>
|
||||
<div class="video-info-private">{{ video.privacy.label }} - {{ getStateLabel(video) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Display only once -->
|
||||
|
|
|
@ -12,6 +12,7 @@ import { AbstractVideoList } from '../../shared/video/abstract-video-list'
|
|||
import { Video } from '../../shared/video/video.model'
|
||||
import { VideoService } from '../../shared/video/video.service'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { VideoState } from '../../../../../shared/models/videos'
|
||||
|
||||
@Component({
|
||||
selector: 'my-account-videos',
|
||||
|
@ -59,7 +60,7 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
|
|||
}
|
||||
|
||||
isInSelectionMode () {
|
||||
return Object.keys(this.checkedVideos).some(k => this.checkedVideos[k] === true)
|
||||
return Object.keys(this.checkedVideos).some(k => this.checkedVideos[ k ] === true)
|
||||
}
|
||||
|
||||
getVideosObservable (page: number) {
|
||||
|
@ -74,47 +75,68 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
|
|||
|
||||
async deleteSelectedVideos () {
|
||||
const toDeleteVideosIds = Object.keys(this.checkedVideos)
|
||||
.filter(k => this.checkedVideos[k] === true)
|
||||
.map(k => parseInt(k, 10))
|
||||
.filter(k => this.checkedVideos[ k ] === true)
|
||||
.map(k => parseInt(k, 10))
|
||||
|
||||
const res = await this.confirmService.confirm(`Do you really want to delete ${toDeleteVideosIds.length} videos?`, 'Delete')
|
||||
const res = await this.confirmService.confirm(
|
||||
this.i18n('Do you really want to delete {{deleteLength}} videos?', { deleteLength: toDeleteVideosIds.length }),
|
||||
this.i18n('Delete')
|
||||
)
|
||||
if (res === false) return
|
||||
|
||||
const observables: Observable<any>[] = []
|
||||
for (const videoId of toDeleteVideosIds) {
|
||||
const o = this.videoService
|
||||
.removeVideo(videoId)
|
||||
const o = this.videoService.removeVideo(videoId)
|
||||
.pipe(tap(() => this.spliceVideosById(videoId)))
|
||||
|
||||
observables.push(o)
|
||||
}
|
||||
|
||||
observableFrom(observables).pipe(
|
||||
concatAll())
|
||||
observableFrom(observables)
|
||||
.pipe(concatAll())
|
||||
.subscribe(
|
||||
res => {
|
||||
this.notificationsService.success('Success', `${toDeleteVideosIds.length} videos deleted.`)
|
||||
this.notificationsService.success(
|
||||
this.i18n('Success'),
|
||||
this.i18n('{{deleteLength}} videos deleted.', { deleteLength: toDeleteVideosIds.length })
|
||||
)
|
||||
|
||||
this.abortSelectionMode()
|
||||
this.reloadVideos()
|
||||
},
|
||||
|
||||
err => this.notificationsService.error('Error', err.message)
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
)
|
||||
}
|
||||
|
||||
async deleteVideo (video: Video) {
|
||||
const res = await this.confirmService.confirm(`Do you really want to delete ${video.name}?`, 'Delete')
|
||||
const res = await this.confirmService.confirm(
|
||||
this.i18n('Do you really want to delete {{videoName}}?', { videoName: video.name }),
|
||||
this.i18n('Delete')
|
||||
)
|
||||
if (res === false) return
|
||||
|
||||
this.videoService.removeVideo(video.id)
|
||||
.subscribe(
|
||||
status => {
|
||||
this.notificationsService.success('Success', `Video ${video.name} deleted.`)
|
||||
this.reloadVideos()
|
||||
},
|
||||
.subscribe(
|
||||
status => {
|
||||
this.notificationsService.success(
|
||||
this.i18n('Success'),
|
||||
this.i18n('Video {{videoName}} deleted.', { videoName: video.name })
|
||||
)
|
||||
this.reloadVideos()
|
||||
},
|
||||
|
||||
error => this.notificationsService.error('Error', error.message)
|
||||
)
|
||||
error => this.notificationsService.error(this.i18n('Error'), error.message)
|
||||
)
|
||||
}
|
||||
|
||||
getStateLabel (video: Video) {
|
||||
if (video.state.id === VideoState.PUBLISHED) return this.i18n('Published')
|
||||
|
||||
if (video.state.id === VideoState.TO_TRANSCODE && video.waitTranscoding === true) return this.i18n('Waiting transcoding')
|
||||
if (video.state.id === VideoState.TO_TRANSCODE) return this.i18n('To transcode')
|
||||
|
||||
return this.i18n('Unknown state')
|
||||
}
|
||||
|
||||
protected buildVideoHeight () {
|
||||
|
@ -124,7 +146,7 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
|
|||
|
||||
private spliceVideosById (id: number) {
|
||||
for (const key of Object.keys(this.loadedPages)) {
|
||||
const videos = this.loadedPages[key]
|
||||
const videos = this.loadedPages[ key ]
|
||||
const index = videos.findIndex(v => v.id === id)
|
||||
|
||||
if (index !== -1) {
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
import { UserRight, VideoChannel, VideoDetails as VideoDetailsServerModel, VideoFile } from '../../../../../shared'
|
||||
import {
|
||||
UserRight,
|
||||
VideoChannel,
|
||||
VideoConstant,
|
||||
VideoDetails as VideoDetailsServerModel,
|
||||
VideoFile,
|
||||
VideoState
|
||||
} from '../../../../../shared'
|
||||
import { AuthUser } from '../../core'
|
||||
import { Video } from '../../shared/video/video.model'
|
||||
import { Account } from '@app/shared/account/account.model'
|
||||
|
@ -12,6 +19,9 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
|
|||
account: Account
|
||||
commentsEnabled: boolean
|
||||
|
||||
waitTranscoding: boolean
|
||||
state: VideoConstant<VideoState>
|
||||
|
||||
likesPercent: number
|
||||
dislikesPercent: number
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { VideoDetails } from './video-details.model'
|
||||
import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum'
|
||||
import { VideoUpdate } from '../../../../../shared/models/videos'
|
||||
|
||||
export class VideoEdit {
|
||||
export class VideoEdit implements VideoUpdate {
|
||||
category: number
|
||||
licence: number
|
||||
language: string
|
||||
|
@ -10,6 +11,7 @@ export class VideoEdit {
|
|||
tags: string[]
|
||||
nsfw: boolean
|
||||
commentsEnabled: boolean
|
||||
waitTranscoding: boolean
|
||||
channelId: number
|
||||
privacy: VideoPrivacy
|
||||
support: string
|
||||
|
@ -32,6 +34,7 @@ export class VideoEdit {
|
|||
this.tags = videoDetails.tags
|
||||
this.nsfw = videoDetails.nsfw
|
||||
this.commentsEnabled = videoDetails.commentsEnabled
|
||||
this.waitTranscoding = videoDetails.waitTranscoding
|
||||
this.channelId = videoDetails.channel.id
|
||||
this.privacy = videoDetails.privacy.id
|
||||
this.support = videoDetails.support
|
||||
|
@ -42,7 +45,7 @@ export class VideoEdit {
|
|||
|
||||
patch (values: Object) {
|
||||
Object.keys(values).forEach((key) => {
|
||||
this[key] = values[key]
|
||||
this[ key ] = values[ key ]
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -57,6 +60,7 @@ export class VideoEdit {
|
|||
tags: this.tags,
|
||||
nsfw: this.nsfw,
|
||||
commentsEnabled: this.commentsEnabled,
|
||||
waitTranscoding: this.waitTranscoding,
|
||||
channelId: this.channelId,
|
||||
privacy: this.privacy
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { User } from '../'
|
||||
import { Video as VideoServerModel, VideoPrivacy } from '../../../../../shared'
|
||||
import { Video as VideoServerModel, VideoPrivacy, VideoState } from '../../../../../shared'
|
||||
import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
|
||||
import { VideoConstant } from '../../../../../shared/models/videos/video.model'
|
||||
import { getAbsoluteAPIUrl } from '../misc/utils'
|
||||
|
@ -36,6 +36,9 @@ export class Video implements VideoServerModel {
|
|||
dislikes: number
|
||||
nsfw: boolean
|
||||
|
||||
waitTranscoding?: boolean
|
||||
state?: VideoConstant<VideoState>
|
||||
|
||||
account: {
|
||||
id: number
|
||||
uuid: string
|
||||
|
@ -58,15 +61,14 @@ export class Video implements VideoServerModel {
|
|||
|
||||
private static createDurationString (duration: number) {
|
||||
const hours = Math.floor(duration / 3600)
|
||||
const minutes = Math.floor(duration % 3600 / 60)
|
||||
const minutes = Math.floor((duration % 3600) / 60)
|
||||
const seconds = duration % 60
|
||||
|
||||
const minutesPadding = minutes >= 10 ? '' : '0'
|
||||
const secondsPadding = seconds >= 10 ? '' : '0'
|
||||
const displayedHours = hours > 0 ? hours.toString() + ':' : ''
|
||||
|
||||
return displayedHours + minutesPadding +
|
||||
minutes.toString() + ':' + secondsPadding + seconds.toString()
|
||||
return displayedHours + minutesPadding + minutes.toString() + ':' + secondsPadding + seconds.toString()
|
||||
}
|
||||
|
||||
constructor (hash: VideoServerModel, translations = {}) {
|
||||
|
@ -78,6 +80,8 @@ export class Video implements VideoServerModel {
|
|||
this.licence = hash.licence
|
||||
this.language = hash.language
|
||||
this.privacy = hash.privacy
|
||||
this.waitTranscoding = hash.waitTranscoding
|
||||
this.state = hash.state
|
||||
this.description = hash.description
|
||||
this.duration = hash.duration
|
||||
this.durationLabel = Video.createDurationString(hash.duration)
|
||||
|
@ -104,6 +108,8 @@ export class Video implements VideoServerModel {
|
|||
this.licence.label = peertubeTranslate(this.licence.label, translations)
|
||||
this.language.label = peertubeTranslate(this.language.label, translations)
|
||||
this.privacy.label = peertubeTranslate(this.privacy.label, translations)
|
||||
|
||||
if (this.state) this.state.label = peertubeTranslate(this.state.label, translations)
|
||||
}
|
||||
|
||||
isVideoNSFWForUser (user: User, serverConfig: ServerConfig) {
|
||||
|
|
|
@ -80,6 +80,7 @@ export class VideoService {
|
|||
privacy: video.privacy,
|
||||
tags: video.tags,
|
||||
nsfw: video.nsfw,
|
||||
waitTranscoding: video.waitTranscoding,
|
||||
commentsEnabled: video.commentsEnabled,
|
||||
thumbnailfile: video.thumbnailfile,
|
||||
previewfile: video.previewfile
|
||||
|
@ -98,11 +99,11 @@ export class VideoService {
|
|||
const req = new HttpRequest('POST', VideoService.BASE_VIDEO_URL + 'upload', video, { reportProgress: true })
|
||||
|
||||
return this.authHttp
|
||||
.request<{ video: { id: number, uuid: string} }>(req)
|
||||
.request<{ video: { id: number, uuid: string } }>(req)
|
||||
.pipe(catchError(this.restExtractor.handleError))
|
||||
}
|
||||
|
||||
getMyVideos (videoPagination: ComponentPagination, sort: VideoSortField): Observable<{ videos: Video[], totalVideos: number}> {
|
||||
getMyVideos (videoPagination: ComponentPagination, sort: VideoSortField): Observable<{ videos: Video[], totalVideos: number }> {
|
||||
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
|
||||
|
||||
let params = new HttpParams()
|
||||
|
@ -120,7 +121,7 @@ export class VideoService {
|
|||
account: Account,
|
||||
videoPagination: ComponentPagination,
|
||||
sort: VideoSortField
|
||||
): Observable<{ videos: Video[], totalVideos: number}> {
|
||||
): Observable<{ videos: Video[], totalVideos: number }> {
|
||||
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
|
||||
|
||||
let params = new HttpParams()
|
||||
|
@ -138,7 +139,7 @@ export class VideoService {
|
|||
videoChannel: VideoChannel,
|
||||
videoPagination: ComponentPagination,
|
||||
sort: VideoSortField
|
||||
): Observable<{ videos: Video[], totalVideos: number}> {
|
||||
): Observable<{ videos: Video[], totalVideos: number }> {
|
||||
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
|
||||
|
||||
let params = new HttpParams()
|
||||
|
@ -156,7 +157,7 @@ export class VideoService {
|
|||
videoPagination: ComponentPagination,
|
||||
sort: VideoSortField,
|
||||
filter?: VideoFilter
|
||||
): Observable<{ videos: Video[], totalVideos: number}> {
|
||||
): Observable<{ videos: Video[], totalVideos: number }> {
|
||||
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
|
||||
|
||||
let params = new HttpParams()
|
||||
|
@ -225,7 +226,7 @@ export class VideoService {
|
|||
search: string,
|
||||
videoPagination: ComponentPagination,
|
||||
sort: VideoSortField
|
||||
): Observable<{ videos: Video[], totalVideos: number}> {
|
||||
): Observable<{ videos: Video[], totalVideos: number }> {
|
||||
const url = VideoService.BASE_VIDEO_URL + 'search'
|
||||
|
||||
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
|
||||
|
@ -295,18 +296,18 @@ export class VideoService {
|
|||
|
||||
private extractVideos (result: ResultList<VideoServerModel>) {
|
||||
return this.serverService.localeObservable
|
||||
.pipe(
|
||||
map(translations => {
|
||||
const videosJson = result.data
|
||||
const totalVideos = result.total
|
||||
const videos: Video[] = []
|
||||
.pipe(
|
||||
map(translations => {
|
||||
const videosJson = result.data
|
||||
const totalVideos = result.total
|
||||
const videos: Video[] = []
|
||||
|
||||
for (const videoJson of videosJson) {
|
||||
videos.push(new Video(videoJson, translations))
|
||||
}
|
||||
for (const videoJson of videosJson) {
|
||||
videos.push(new Video(videoJson, translations))
|
||||
}
|
||||
|
||||
return { videos, totalVideos }
|
||||
})
|
||||
)
|
||||
return { videos, totalVideos }
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -109,6 +109,16 @@
|
|||
<label i18n for="commentsEnabled">Enable video comments</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group form-group-checkbox">
|
||||
<input type="checkbox" id="waitTranscoding" formControlName="waitTranscoding" />
|
||||
<label for="waitTranscoding"></label>
|
||||
<label i18n for="waitTranscoding">Wait transcoding before publishing the video</label>
|
||||
<my-help
|
||||
tooltipPlacement="top" helpType="custom" i18n-customHtml
|
||||
customHtml="If you decide to not wait transcoding before publishing the video, it can be unplayable until it transcoding ends."
|
||||
></my-help>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</tab>
|
||||
|
||||
|
|
|
@ -47,6 +47,7 @@ export class VideoEditComponent implements OnInit {
|
|||
const defaultValues = {
|
||||
nsfw: 'false',
|
||||
commentsEnabled: 'true',
|
||||
waitTranscoding: 'true',
|
||||
tags: []
|
||||
}
|
||||
const obj = {
|
||||
|
@ -55,6 +56,7 @@ export class VideoEditComponent implements OnInit {
|
|||
channelId: this.videoValidatorsService.VIDEO_CHANNEL,
|
||||
nsfw: null,
|
||||
commentsEnabled: null,
|
||||
waitTranscoding: null,
|
||||
category: this.videoValidatorsService.VIDEO_CATEGORY,
|
||||
licence: this.videoValidatorsService.VIDEO_LICENCE,
|
||||
language: this.videoValidatorsService.VIDEO_LANGUAGE,
|
||||
|
@ -74,13 +76,13 @@ export class VideoEditComponent implements OnInit {
|
|||
)
|
||||
|
||||
// We will update the "support" field depending on the channel
|
||||
this.form.controls['channelId']
|
||||
this.form.controls[ 'channelId' ]
|
||||
.valueChanges
|
||||
.pipe(map(res => parseInt(res.toString(), 10)))
|
||||
.subscribe(
|
||||
newChannelId => {
|
||||
const oldChannelId = parseInt(this.form.value['channelId'], 10)
|
||||
const currentSupport = this.form.value['support']
|
||||
const oldChannelId = parseInt(this.form.value[ 'channelId' ], 10)
|
||||
const currentSupport = this.form.value[ 'support' ]
|
||||
|
||||
// Not initialized yet
|
||||
if (isNaN(newChannelId)) return
|
||||
|
|
|
@ -164,6 +164,7 @@ export class VideoAddComponent extends FormReactive implements OnInit, OnDestroy
|
|||
|
||||
const privacy = this.firstStepPrivacyId.toString()
|
||||
const nsfw = false
|
||||
const waitTranscoding = true
|
||||
const commentsEnabled = true
|
||||
const channelId = this.firstStepChannelId.toString()
|
||||
|
||||
|
@ -173,6 +174,7 @@ export class VideoAddComponent extends FormReactive implements OnInit, OnDestroy
|
|||
formData.append('privacy', VideoPrivacy.PRIVATE.toString())
|
||||
formData.append('nsfw', '' + nsfw)
|
||||
formData.append('commentsEnabled', '' + commentsEnabled)
|
||||
formData.append('waitTranscoding', '' + waitTranscoding)
|
||||
formData.append('channelId', '' + channelId)
|
||||
formData.append('videofile', videofile)
|
||||
|
||||
|
|
|
@ -3,6 +3,10 @@
|
|||
<div id="video-element-wrapper">
|
||||
</div>
|
||||
|
||||
<div i18n id="warning-transcoding" class="alert alert-warning" *ngIf="isVideoToTranscode()">
|
||||
The video is being transcoded, it may not work properly.
|
||||
</div>
|
||||
|
||||
<!-- Video information -->
|
||||
<div *ngIf="video" class="margin-content video-bottom">
|
||||
<div class="video-info">
|
||||
|
|
|
@ -28,6 +28,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
#warning-transcoding {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#video-not-found {
|
||||
height: 300px;
|
||||
line-height: 300px;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { catchError } from 'rxjs/operators'
|
||||
import { Component, ElementRef, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild, Inject } from '@angular/core'
|
||||
import { Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { RedirectService } from '@app/core/routing/redirect.service'
|
||||
import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage'
|
||||
|
@ -10,7 +10,7 @@ import { Subscription } from 'rxjs'
|
|||
import * as videojs from 'video.js'
|
||||
import 'videojs-hotkeys'
|
||||
import * as WebTorrent from 'webtorrent'
|
||||
import { UserVideoRateType, VideoRateType } from '../../../../../shared'
|
||||
import { UserVideoRateType, VideoRateType, VideoState } from '../../../../../shared'
|
||||
import '../../../assets/player/peertube-videojs-plugin'
|
||||
import { AuthService, ConfirmService } from '../../core'
|
||||
import { RestExtractor, VideoBlacklistService } from '../../shared'
|
||||
|
@ -21,7 +21,7 @@ import { MarkdownService } from '../shared'
|
|||
import { VideoDownloadComponent } from './modal/video-download.component'
|
||||
import { VideoReportComponent } from './modal/video-report.component'
|
||||
import { VideoShareComponent } from './modal/video-share.component'
|
||||
import { getVideojsOptions, loadLocale, addContextMenu } from '../../../assets/player/peertube-player'
|
||||
import { addContextMenu, getVideojsOptions, loadLocale } from '../../../assets/player/peertube-player'
|
||||
import { ServerService } from '@app/core'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { environment } from '../../../environments/environment'
|
||||
|
@ -91,21 +91,21 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
this.videoService.getVideos({ currentPage: 1, itemsPerPage: 5 }, '-createdAt')
|
||||
.subscribe(
|
||||
data => {
|
||||
this.otherVideos = data.videos
|
||||
this.updateOtherVideosDisplayed()
|
||||
},
|
||||
.subscribe(
|
||||
data => {
|
||||
this.otherVideos = data.videos
|
||||
this.updateOtherVideosDisplayed()
|
||||
},
|
||||
|
||||
err => console.error(err)
|
||||
)
|
||||
err => console.error(err)
|
||||
)
|
||||
|
||||
this.paramsSub = this.route.params.subscribe(routeParams => {
|
||||
if (this.player) {
|
||||
this.player.pause()
|
||||
}
|
||||
|
||||
const uuid = routeParams['uuid']
|
||||
const uuid = routeParams[ 'uuid' ]
|
||||
|
||||
// Video did not change
|
||||
if (this.video && this.video.uuid === uuid) return
|
||||
|
@ -113,13 +113,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
this.videoService
|
||||
.getVideo(uuid)
|
||||
.pipe(catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 404 ])))
|
||||
.subscribe(
|
||||
video => {
|
||||
const startTime = this.route.snapshot.queryParams.start
|
||||
this.onVideoFetched(video, startTime)
|
||||
.catch(err => this.handleError(err))
|
||||
}
|
||||
)
|
||||
.subscribe(video => {
|
||||
const startTime = this.route.snapshot.queryParams.start
|
||||
this.onVideoFetched(video, startTime)
|
||||
.catch(err => this.handleError(err))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -157,17 +155,17 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
if (res === false) return
|
||||
|
||||
this.videoBlacklistService.blacklistVideo(this.video.id)
|
||||
.subscribe(
|
||||
status => {
|
||||
this.notificationsService.success(
|
||||
this.i18n('Success'),
|
||||
this.i18n('Video {{videoName}} had been blacklisted.', { videoName: this.video.name })
|
||||
)
|
||||
this.redirectService.redirectToHomepage()
|
||||
},
|
||||
.subscribe(
|
||||
() => {
|
||||
this.notificationsService.success(
|
||||
this.i18n('Success'),
|
||||
this.i18n('Video {{videoName}} had been blacklisted.', { videoName: this.video.name })
|
||||
)
|
||||
this.redirectService.redirectToHomepage()
|
||||
},
|
||||
|
||||
error => this.notificationsService.error(this.i18n('Error'), error.message)
|
||||
)
|
||||
error => this.notificationsService.error(this.i18n('Error'), error.message)
|
||||
)
|
||||
}
|
||||
|
||||
showMoreDescription () {
|
||||
|
@ -188,22 +186,22 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
this.descriptionLoading = true
|
||||
|
||||
this.videoService.loadCompleteDescription(this.video.descriptionPath)
|
||||
.subscribe(
|
||||
description => {
|
||||
this.completeDescriptionShown = true
|
||||
this.descriptionLoading = false
|
||||
.subscribe(
|
||||
description => {
|
||||
this.completeDescriptionShown = true
|
||||
this.descriptionLoading = false
|
||||
|
||||
this.shortVideoDescription = this.video.description
|
||||
this.completeVideoDescription = description
|
||||
this.shortVideoDescription = this.video.description
|
||||
this.completeVideoDescription = description
|
||||
|
||||
this.updateVideoDescription(this.completeVideoDescription)
|
||||
},
|
||||
this.updateVideoDescription(this.completeVideoDescription)
|
||||
},
|
||||
|
||||
error => {
|
||||
this.descriptionLoading = false
|
||||
this.notificationsService.error(this.i18n('Error'), error.message)
|
||||
}
|
||||
)
|
||||
error => {
|
||||
this.descriptionLoading = false
|
||||
this.notificationsService.error(this.i18n('Error'), error.message)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
showReportModal (event: Event) {
|
||||
|
@ -259,19 +257,19 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
if (res === false) return
|
||||
|
||||
this.videoService.removeVideo(this.video.id)
|
||||
.subscribe(
|
||||
status => {
|
||||
this.notificationsService.success(
|
||||
this.i18n('Success'),
|
||||
this.i18n('Video {{videoName}} deleted.', { videoName: this.video.name })
|
||||
)
|
||||
.subscribe(
|
||||
status => {
|
||||
this.notificationsService.success(
|
||||
this.i18n('Success'),
|
||||
this.i18n('Video {{videoName}} deleted.', { videoName: this.video.name })
|
||||
)
|
||||
|
||||
// Go back to the video-list.
|
||||
this.redirectService.redirectToHomepage()
|
||||
},
|
||||
// Go back to the video-list.
|
||||
this.redirectService.redirectToHomepage()
|
||||
},
|
||||
|
||||
error => this.notificationsService.error(this.i18n('Error'), error.message)
|
||||
)
|
||||
error => this.notificationsService.error(this.i18n('Error'), error.message)
|
||||
)
|
||||
}
|
||||
|
||||
acceptedPrivacyConcern () {
|
||||
|
@ -279,6 +277,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
this.hasAlreadyAcceptedPrivacyConcern = true
|
||||
}
|
||||
|
||||
isVideoToTranscode () {
|
||||
return this.video && this.video.state.id === VideoState.TO_TRANSCODE
|
||||
}
|
||||
|
||||
private updateVideoDescription (description: string) {
|
||||
this.video.description = description
|
||||
this.setVideoDescriptionHTML()
|
||||
|
@ -294,10 +296,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
private setVideoLikesBarTooltipText () {
|
||||
this.likesBarTooltipText = this.i18n(
|
||||
'{{likesNumber}} likes / {{dislikesNumber}} dislikes',
|
||||
{ likesNumber: this.video.likes, dislikesNumber: this.video.dislikes }
|
||||
)
|
||||
this.likesBarTooltipText = this.i18n('{{likesNumber}} likes / {{dislikesNumber}} dislikes', {
|
||||
likesNumber: this.video.likes,
|
||||
dislikesNumber: this.video.dislikes
|
||||
})
|
||||
}
|
||||
|
||||
private handleError (err: any) {
|
||||
|
@ -320,15 +322,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
if (this.isUserLoggedIn() === false) return
|
||||
|
||||
this.videoService.getUserVideoRating(this.video.id)
|
||||
.subscribe(
|
||||
ratingObject => {
|
||||
if (ratingObject) {
|
||||
this.userRating = ratingObject.rating
|
||||
}
|
||||
},
|
||||
.subscribe(
|
||||
ratingObject => {
|
||||
if (ratingObject) {
|
||||
this.userRating = ratingObject.rating
|
||||
}
|
||||
},
|
||||
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
)
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
)
|
||||
}
|
||||
|
||||
private async onVideoFetched (video: VideoDetails, startTime = 0) {
|
||||
|
@ -409,14 +411,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
method.call(this.videoService, this.video.id)
|
||||
.subscribe(
|
||||
() => {
|
||||
// Update the video like attribute
|
||||
this.updateVideoRating(this.userRating, nextRating)
|
||||
this.userRating = nextRating
|
||||
},
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
)
|
||||
.subscribe(
|
||||
() => {
|
||||
// Update the video like attribute
|
||||
this.updateVideoRating(this.userRating, nextRating)
|
||||
this.userRating = nextRating
|
||||
},
|
||||
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
)
|
||||
}
|
||||
|
||||
private updateVideoRating (oldRating: UserVideoRateType, newRating: VideoRateType) {
|
||||
|
|
|
@ -68,7 +68,6 @@
|
|||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{css,md}": "precise-commits",
|
||||
"*.scss": [
|
||||
"sass-lint -c .sass-lint.yml",
|
||||
"git add"
|
||||
|
@ -166,7 +165,6 @@
|
|||
"maildev": "^1.0.0-rc3",
|
||||
"mocha": "^5.0.0",
|
||||
"nodemon": "^1.11.0",
|
||||
"precise-commits": "^1.0.2",
|
||||
"prettier": "1.13.2",
|
||||
"prompt": "^1.0.0",
|
||||
"sass-lint": "^1.12.1",
|
||||
|
|
|
@ -123,11 +123,11 @@ async function accountFollowingController (req: express.Request, res: express.Re
|
|||
async function videoController (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
const video: VideoModel = res.locals.video
|
||||
|
||||
const audience = await getAudience(video.VideoChannel.Account.Actor, undefined, video.privacy === VideoPrivacy.PUBLIC)
|
||||
const audience = getAudience(video.VideoChannel.Account.Actor, video.privacy === VideoPrivacy.PUBLIC)
|
||||
const videoObject = audiencify(video.toActivityPubObject(), audience)
|
||||
|
||||
if (req.path.endsWith('/activity')) {
|
||||
const data = await createActivityData(video.url, video.VideoChannel.Account.Actor, videoObject, undefined, audience)
|
||||
const data = createActivityData(video.url, video.VideoChannel.Account.Actor, videoObject, audience)
|
||||
return activityPubResponse(activityPubContextify(data), res)
|
||||
}
|
||||
|
||||
|
@ -210,12 +210,12 @@ async function videoCommentController (req: express.Request, res: express.Respon
|
|||
|
||||
const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined)
|
||||
const isPublic = true // Comments are always public
|
||||
const audience = await getAudience(videoComment.Account.Actor, undefined, isPublic)
|
||||
const audience = getAudience(videoComment.Account.Actor, isPublic)
|
||||
|
||||
const videoCommentObject = audiencify(videoComment.toActivityPubObject(threadParentComments), audience)
|
||||
|
||||
if (req.path.endsWith('/activity')) {
|
||||
const data = await createActivityData(videoComment.url, videoComment.Account.Actor, videoCommentObject, undefined, audience)
|
||||
const data = createActivityData(videoComment.url, videoComment.Account.Actor, videoCommentObject, audience)
|
||||
return activityPubResponse(activityPubContextify(data), res)
|
||||
}
|
||||
|
||||
|
|
|
@ -54,12 +54,12 @@ async function buildActivities (actor: ActorModel, start: number, count: number)
|
|||
// This is a shared video
|
||||
if (video.VideoShares !== undefined && video.VideoShares.length !== 0) {
|
||||
const videoShare = video.VideoShares[0]
|
||||
const announceActivity = await announceActivityData(videoShare.url, actor, video.url, undefined, createActivityAudience)
|
||||
const announceActivity = announceActivityData(videoShare.url, actor, video.url, createActivityAudience)
|
||||
|
||||
activities.push(announceActivity)
|
||||
} else {
|
||||
const videoObject = video.toActivityPubObject()
|
||||
const createActivity = await createActivityData(video.url, byActor, videoObject, undefined, createActivityAudience)
|
||||
const createActivity = createActivityData(video.url, byActor, videoObject, createActivityAudience)
|
||||
|
||||
activities.push(createActivity)
|
||||
}
|
||||
|
|
|
@ -166,7 +166,7 @@ export {
|
|||
|
||||
async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
const user = res.locals.oauth.token.User as UserModel
|
||||
const resultList = await VideoModel.listAccountVideosForApi(
|
||||
const resultList = await VideoModel.listUserVideosForApi(
|
||||
user.Account.id,
|
||||
req.query.start as number,
|
||||
req.query.count as number,
|
||||
|
@ -174,7 +174,8 @@ async function getUserVideos (req: express.Request, res: express.Response, next:
|
|||
false // Display my NSFW videos
|
||||
)
|
||||
|
||||
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||
const additionalAttributes = { waitTranscoding: true, state: true }
|
||||
return res.json(getFormattedObjects(resultList.data, resultList.total, { additionalAttributes }))
|
||||
}
|
||||
|
||||
async function createUserRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
|
@ -318,7 +319,7 @@ async function updateMe (req: express.Request, res: express.Response, next: expr
|
|||
}
|
||||
|
||||
async function updateMyAvatar (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
const avatarPhysicalFile = req.files['avatarfile'][0]
|
||||
const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ]
|
||||
const user = res.locals.oauth.token.user
|
||||
const actor = user.Account.Actor
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as express from 'express'
|
||||
import { extname, join } from 'path'
|
||||
import { VideoCreate, VideoPrivacy, VideoUpdate } from '../../../../shared'
|
||||
import { VideoCreate, VideoPrivacy, VideoState, VideoUpdate } from '../../../../shared'
|
||||
import { renamePromise } from '../../../helpers/core-utils'
|
||||
import { retryTransactionWrapper } from '../../../helpers/database-utils'
|
||||
import { getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
|
||||
|
@ -21,11 +21,11 @@ import {
|
|||
} from '../../../initializers'
|
||||
import {
|
||||
changeVideoChannelShare,
|
||||
federateVideoIfNeeded,
|
||||
fetchRemoteVideoDescription,
|
||||
getVideoActivityPubUrl,
|
||||
shareVideoByServerAndChannel
|
||||
getVideoActivityPubUrl
|
||||
} from '../../../lib/activitypub'
|
||||
import { sendCreateVideo, sendCreateView, sendUpdateVideo } from '../../../lib/activitypub/send'
|
||||
import { sendCreateView } from '../../../lib/activitypub/send'
|
||||
import { JobQueue } from '../../../lib/job-queue'
|
||||
import { Redis } from '../../../lib/redis'
|
||||
import {
|
||||
|
@ -51,7 +51,7 @@ import { videoCommentRouter } from './comment'
|
|||
import { rateVideoRouter } from './rate'
|
||||
import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
|
||||
import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type'
|
||||
import { isNSFWHidden, createReqFiles } from '../../../helpers/express-utils'
|
||||
import { createReqFiles, isNSFWHidden } from '../../../helpers/express-utils'
|
||||
|
||||
const videosRouter = express.Router()
|
||||
|
||||
|
@ -185,8 +185,10 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
|
|||
category: videoInfo.category,
|
||||
licence: videoInfo.licence,
|
||||
language: videoInfo.language,
|
||||
commentsEnabled: videoInfo.commentsEnabled,
|
||||
nsfw: videoInfo.nsfw,
|
||||
commentsEnabled: videoInfo.commentsEnabled || false,
|
||||
waitTranscoding: videoInfo.waitTranscoding || false,
|
||||
state: CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED,
|
||||
nsfw: videoInfo.nsfw || false,
|
||||
description: videoInfo.description,
|
||||
support: videoInfo.support,
|
||||
privacy: videoInfo.privacy,
|
||||
|
@ -194,19 +196,20 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
|
|||
channelId: res.locals.videoChannel.id
|
||||
}
|
||||
const video = new VideoModel(videoData)
|
||||
video.url = getVideoActivityPubUrl(video)
|
||||
video.url = getVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
|
||||
|
||||
// Build the file object
|
||||
const { videoFileResolution } = await getVideoFileResolution(videoPhysicalFile.path)
|
||||
|
||||
const videoFileData = {
|
||||
extname: extname(videoPhysicalFile.filename),
|
||||
resolution: videoFileResolution,
|
||||
size: videoPhysicalFile.size
|
||||
}
|
||||
const videoFile = new VideoFileModel(videoFileData)
|
||||
|
||||
// Move physical file
|
||||
const videoDir = CONFIG.STORAGE.VIDEOS_DIR
|
||||
const destination = join(videoDir, video.getVideoFilename(videoFile))
|
||||
|
||||
await renamePromise(videoPhysicalFile.path, destination)
|
||||
// This is important in case if there is another attempt in the retry process
|
||||
videoPhysicalFile.filename = video.getVideoFilename(videoFile)
|
||||
|
@ -230,6 +233,7 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
|
|||
await video.createPreview(videoFile)
|
||||
}
|
||||
|
||||
// Create the torrent file
|
||||
await video.createTorrentAndSetInfoHash(videoFile)
|
||||
|
||||
const videoCreated = await sequelizeTypescript.transaction(async t => {
|
||||
|
@ -251,20 +255,14 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi
|
|||
video.Tags = tagInstances
|
||||
}
|
||||
|
||||
// Let transcoding job send the video to friends because the video file extension might change
|
||||
if (CONFIG.TRANSCODING.ENABLED === true) return videoCreated
|
||||
// Don't send video to remote servers, it is private
|
||||
if (video.privacy === VideoPrivacy.PRIVATE) return videoCreated
|
||||
|
||||
await sendCreateVideo(video, t)
|
||||
await shareVideoByServerAndChannel(video, t)
|
||||
await federateVideoIfNeeded(video, true, t)
|
||||
|
||||
logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid)
|
||||
|
||||
return videoCreated
|
||||
})
|
||||
|
||||
if (CONFIG.TRANSCODING.ENABLED === true) {
|
||||
if (video.state === VideoState.TO_TRANSCODE) {
|
||||
// Put uuid because we don't have id auto incremented for now
|
||||
const dataInput = {
|
||||
videoUUID: videoCreated.uuid,
|
||||
|
@ -318,6 +316,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
|
|||
if (videoInfoToUpdate.licence !== undefined) videoInstance.set('licence', videoInfoToUpdate.licence)
|
||||
if (videoInfoToUpdate.language !== undefined) videoInstance.set('language', videoInfoToUpdate.language)
|
||||
if (videoInfoToUpdate.nsfw !== undefined) videoInstance.set('nsfw', videoInfoToUpdate.nsfw)
|
||||
if (videoInfoToUpdate.waitTranscoding !== undefined) videoInstance.set('waitTranscoding', videoInfoToUpdate.waitTranscoding)
|
||||
if (videoInfoToUpdate.support !== undefined) videoInstance.set('support', videoInfoToUpdate.support)
|
||||
if (videoInfoToUpdate.description !== undefined) videoInstance.set('description', videoInfoToUpdate.description)
|
||||
if (videoInfoToUpdate.commentsEnabled !== undefined) videoInstance.set('commentsEnabled', videoInfoToUpdate.commentsEnabled)
|
||||
|
@ -343,19 +342,13 @@ async function updateVideo (req: express.Request, res: express.Response) {
|
|||
// Video channel update?
|
||||
if (res.locals.videoChannel && videoInstanceUpdated.channelId !== res.locals.videoChannel.id) {
|
||||
await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel, { transaction: t })
|
||||
videoInstance.VideoChannel = res.locals.videoChannel
|
||||
videoInstanceUpdated.VideoChannel = res.locals.videoChannel
|
||||
|
||||
if (wasPrivateVideo === false) await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t)
|
||||
}
|
||||
|
||||
// Now we'll update the video's meta data to our friends
|
||||
if (wasPrivateVideo === false) await sendUpdateVideo(videoInstanceUpdated, t)
|
||||
|
||||
// Video is not private anymore, send a create action to remote servers
|
||||
if (wasPrivateVideo === true && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE) {
|
||||
await sendCreateVideo(videoInstanceUpdated, t)
|
||||
await shareVideoByServerAndChannel(videoInstanceUpdated, t)
|
||||
}
|
||||
const isNewVideo = wasPrivateVideo && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE
|
||||
await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo)
|
||||
})
|
||||
|
||||
logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid)
|
||||
|
|
|
@ -8,22 +8,24 @@ import { signObject } from './peertube-crypto'
|
|||
import { pageToStartAndCount } from './core-utils'
|
||||
|
||||
function activityPubContextify <T> (data: T) {
|
||||
return Object.assign(data,{
|
||||
return Object.assign(data, {
|
||||
'@context': [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://w3id.org/security/v1',
|
||||
{
|
||||
'RsaSignature2017': 'https://w3id.org/security#RsaSignature2017',
|
||||
'Hashtag': 'as:Hashtag',
|
||||
'uuid': 'http://schema.org/identifier',
|
||||
'category': 'http://schema.org/category',
|
||||
'licence': 'http://schema.org/license',
|
||||
'sensitive': 'as:sensitive',
|
||||
'language': 'http://schema.org/inLanguage',
|
||||
'views': 'http://schema.org/Number',
|
||||
'size': 'http://schema.org/Number',
|
||||
'commentsEnabled': 'http://schema.org/Boolean',
|
||||
'support': 'http://schema.org/Text'
|
||||
RsaSignature2017: 'https://w3id.org/security#RsaSignature2017',
|
||||
Hashtag: 'as:Hashtag',
|
||||
uuid: 'http://schema.org/identifier',
|
||||
category: 'http://schema.org/category',
|
||||
licence: 'http://schema.org/license',
|
||||
sensitive: 'as:sensitive',
|
||||
language: 'http://schema.org/inLanguage',
|
||||
views: 'http://schema.org/Number',
|
||||
stats: 'http://schema.org/Number',
|
||||
size: 'http://schema.org/Number',
|
||||
commentsEnabled: 'http://schema.org/Boolean',
|
||||
waitTranscoding: 'http://schema.org/Boolean',
|
||||
support: 'http://schema.org/Text'
|
||||
},
|
||||
{
|
||||
likes: {
|
||||
|
|
|
@ -6,11 +6,13 @@ import {
|
|||
isVideoAbuseReasonValid,
|
||||
isVideoDurationValid,
|
||||
isVideoNameValid,
|
||||
isVideoStateValid,
|
||||
isVideoTagValid,
|
||||
isVideoTruncatedDescriptionValid,
|
||||
isVideoViewsValid
|
||||
} from '../videos'
|
||||
import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
|
||||
import { VideoState } from '../../../../shared/models/videos'
|
||||
|
||||
function sanitizeAndCheckVideoTorrentCreateActivity (activity: any) {
|
||||
return isBaseActivityValid(activity, 'Create') &&
|
||||
|
@ -50,6 +52,10 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
|
|||
if (!setRemoteVideoTruncatedContent(video)) return false
|
||||
if (!setValidAttributedTo(video)) return false
|
||||
|
||||
// Default attributes
|
||||
if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED
|
||||
if (!isBooleanValid(video.waitTranscoding)) video.waitTranscoding = false
|
||||
|
||||
return isActivityPubUrlValid(video.id) &&
|
||||
isVideoNameValid(video.name) &&
|
||||
isActivityPubVideoDurationValid(video.duration) &&
|
||||
|
|
|
@ -10,7 +10,8 @@ import {
|
|||
VIDEO_LICENCES,
|
||||
VIDEO_MIMETYPE_EXT,
|
||||
VIDEO_PRIVACIES,
|
||||
VIDEO_RATE_TYPES
|
||||
VIDEO_RATE_TYPES,
|
||||
VIDEO_STATES
|
||||
} from '../../initializers'
|
||||
import { VideoModel } from '../../models/video/video'
|
||||
import { exists, isArray, isFileValid } from './misc'
|
||||
|
@ -21,11 +22,15 @@ const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS
|
|||
const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES
|
||||
|
||||
function isVideoCategoryValid (value: any) {
|
||||
return value === null || VIDEO_CATEGORIES[value] !== undefined
|
||||
return value === null || VIDEO_CATEGORIES[ value ] !== undefined
|
||||
}
|
||||
|
||||
function isVideoStateValid (value: any) {
|
||||
return exists(value) && VIDEO_STATES[ value ] !== undefined
|
||||
}
|
||||
|
||||
function isVideoLicenceValid (value: any) {
|
||||
return value === null || VIDEO_LICENCES[value] !== undefined
|
||||
return value === null || VIDEO_LICENCES[ value ] !== undefined
|
||||
}
|
||||
|
||||
function isVideoLanguageValid (value: any) {
|
||||
|
@ -79,20 +84,22 @@ function isVideoRatingTypeValid (value: string) {
|
|||
|
||||
const videoFileTypes = Object.keys(VIDEO_MIMETYPE_EXT).map(m => `(${m})`)
|
||||
const videoFileTypesRegex = videoFileTypes.join('|')
|
||||
|
||||
function isVideoFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) {
|
||||
return isFileValid(files, videoFileTypesRegex, 'videofile')
|
||||
}
|
||||
|
||||
const videoImageTypes = CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME
|
||||
.map(v => v.replace('.', ''))
|
||||
.join('|')
|
||||
.map(v => v.replace('.', ''))
|
||||
.join('|')
|
||||
const videoImageTypesRegex = `image/(${videoImageTypes})`
|
||||
|
||||
function isVideoImage (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], field: string) {
|
||||
return isFileValid(files, videoImageTypesRegex, field, true)
|
||||
}
|
||||
|
||||
function isVideoPrivacyValid (value: string) {
|
||||
return validator.isInt(value + '') && VIDEO_PRIVACIES[value] !== undefined
|
||||
return validator.isInt(value + '') && VIDEO_PRIVACIES[ value ] !== undefined
|
||||
}
|
||||
|
||||
function isVideoFileInfoHashValid (value: string) {
|
||||
|
@ -118,8 +125,8 @@ async function isVideoExist (id: string, res: Response) {
|
|||
|
||||
if (!video) {
|
||||
res.status(404)
|
||||
.json({ error: 'Video not found' })
|
||||
.end()
|
||||
.json({ error: 'Video not found' })
|
||||
.end()
|
||||
|
||||
return false
|
||||
}
|
||||
|
@ -169,6 +176,7 @@ export {
|
|||
isVideoTagsValid,
|
||||
isVideoAbuseReasonValid,
|
||||
isVideoFile,
|
||||
isVideoStateValid,
|
||||
isVideoViewsValid,
|
||||
isVideoRatingTypeValid,
|
||||
isVideoDurationValid,
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { Model } from 'sequelize-typescript'
|
||||
import * as ipaddr from 'ipaddr.js'
|
||||
const isCidr = require('is-cidr')
|
||||
import { ResultList } from '../../shared'
|
||||
import { VideoResolution } from '../../shared/models/videos'
|
||||
import { CONFIG } from '../initializers'
|
||||
|
@ -10,6 +9,8 @@ import { ApplicationModel } from '../models/application/application'
|
|||
import { pseudoRandomBytesPromise } from './core-utils'
|
||||
import { logger } from './logger'
|
||||
|
||||
const isCidr = require('is-cidr')
|
||||
|
||||
async function generateRandomString (size: number) {
|
||||
const raw = await pseudoRandomBytesPromise(size)
|
||||
|
||||
|
@ -17,22 +18,20 @@ async function generateRandomString (size: number) {
|
|||
}
|
||||
|
||||
interface FormattableToJSON {
|
||||
toFormattedJSON ()
|
||||
toFormattedJSON (args?: any)
|
||||
}
|
||||
|
||||
function getFormattedObjects<U, T extends FormattableToJSON> (objects: T[], objectsTotal: number) {
|
||||
function getFormattedObjects<U, T extends FormattableToJSON> (objects: T[], objectsTotal: number, formattedArg?: any) {
|
||||
const formattedObjects: U[] = []
|
||||
|
||||
objects.forEach(object => {
|
||||
formattedObjects.push(object.toFormattedJSON())
|
||||
formattedObjects.push(object.toFormattedJSON(formattedArg))
|
||||
})
|
||||
|
||||
const res: ResultList<U> = {
|
||||
return {
|
||||
total: objectsTotal,
|
||||
data: formattedObjects
|
||||
}
|
||||
|
||||
return res
|
||||
} as ResultList<U>
|
||||
}
|
||||
|
||||
async function isSignupAllowed () {
|
||||
|
@ -87,16 +86,17 @@ function computeResolutionsToTranscode (videoFileHeight: number) {
|
|||
const resolutionsEnabled: number[] = []
|
||||
const configResolutions = CONFIG.TRANSCODING.RESOLUTIONS
|
||||
|
||||
// Put in the order we want to proceed jobs
|
||||
const resolutions = [
|
||||
VideoResolution.H_240P,
|
||||
VideoResolution.H_360P,
|
||||
VideoResolution.H_480P,
|
||||
VideoResolution.H_360P,
|
||||
VideoResolution.H_720P,
|
||||
VideoResolution.H_240P,
|
||||
VideoResolution.H_1080P
|
||||
]
|
||||
|
||||
for (const resolution of resolutions) {
|
||||
if (configResolutions[resolution + 'p'] === true && videoFileHeight > resolution) {
|
||||
if (configResolutions[ resolution + 'p' ] === true && videoFileHeight > resolution) {
|
||||
resolutionsEnabled.push(resolution)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { IConfig } from 'config'
|
||||
import { dirname, join } from 'path'
|
||||
import { JobType, VideoRateType } from '../../shared/models'
|
||||
import { JobType, VideoRateType, VideoState } from '../../shared/models'
|
||||
import { ActivityPubActorType } from '../../shared/models/activitypub'
|
||||
import { FollowState } from '../../shared/models/actors'
|
||||
import { VideoPrivacy } from '../../shared/models/videos'
|
||||
|
@ -14,7 +14,7 @@ let config: IConfig = require('config')
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const LAST_MIGRATION_VERSION = 215
|
||||
const LAST_MIGRATION_VERSION = 220
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
@ -326,6 +326,11 @@ const VIDEO_PRIVACIES = {
|
|||
[VideoPrivacy.PRIVATE]: 'Private'
|
||||
}
|
||||
|
||||
const VIDEO_STATES = {
|
||||
[VideoState.PUBLISHED]: 'Published',
|
||||
[VideoState.TO_TRANSCODE]: 'To transcode'
|
||||
}
|
||||
|
||||
const VIDEO_MIMETYPE_EXT = {
|
||||
'video/webm': '.webm',
|
||||
'video/ogg': '.ogv',
|
||||
|
@ -493,6 +498,7 @@ export {
|
|||
VIDEO_LANGUAGES,
|
||||
VIDEO_PRIVACIES,
|
||||
VIDEO_LICENCES,
|
||||
VIDEO_STATES,
|
||||
VIDEO_RATE_TYPES,
|
||||
VIDEO_MIMETYPE_EXT,
|
||||
VIDEO_TRANSCODING_FPS,
|
||||
|
|
|
@ -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 }
|
|
@ -20,7 +20,7 @@ function getVideoCommentAudience (
|
|||
isOrigin = false
|
||||
) {
|
||||
const to = [ ACTIVITY_PUB.PUBLIC ]
|
||||
const cc = [ ]
|
||||
const cc = []
|
||||
|
||||
// Owner of the video we comment
|
||||
if (isOrigin === false) {
|
||||
|
@ -55,7 +55,7 @@ async function getActorsInvolvedInVideo (video: VideoModel, t: Transaction) {
|
|||
return actors
|
||||
}
|
||||
|
||||
async function getAudience (actorSender: ActorModel, t: Transaction, isPublic = true) {
|
||||
function getAudience (actorSender: ActorModel, isPublic = true) {
|
||||
return buildAudience([ actorSender.followersUrl ], isPublic)
|
||||
}
|
||||
|
||||
|
@ -67,14 +67,14 @@ function buildAudience (followerUrls: string[], isPublic = true) {
|
|||
to = [ ACTIVITY_PUB.PUBLIC ]
|
||||
cc = followerUrls
|
||||
} else { // Unlisted
|
||||
to = [ ]
|
||||
cc = [ ]
|
||||
to = []
|
||||
cc = []
|
||||
}
|
||||
|
||||
return { to, cc }
|
||||
}
|
||||
|
||||
function audiencify <T> (object: T, audience: ActivityAudience) {
|
||||
function audiencify<T> (object: T, audience: ActivityAudience) {
|
||||
return Object.assign(object, audience)
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Pr
|
|||
|
||||
if (Array.isArray(body.orderedItems)) {
|
||||
const items = body.orderedItems
|
||||
logger.info('Processing %i ActivityPub items for %s.', items.length, nextLink)
|
||||
logger.info('Processing %i ActivityPub items for %s.', items.length, options.uri)
|
||||
|
||||
await handler(items)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import * as Bluebird from 'bluebird'
|
||||
import { ActivityUpdate } from '../../../../shared/models/activitypub'
|
||||
import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor'
|
||||
import { VideoTorrentObject } from '../../../../shared/models/activitypub/objects'
|
||||
import { retryTransactionWrapper } from '../../../helpers/database-utils'
|
||||
import { logger } from '../../../helpers/logger'
|
||||
import { resetSequelizeInstance } from '../../../helpers/utils'
|
||||
|
@ -13,6 +12,7 @@ import { VideoChannelModel } from '../../../models/video/video-channel'
|
|||
import { VideoFileModel } from '../../../models/video/video-file'
|
||||
import { fetchAvatarIfExists, getOrCreateActorAndServerAndModel, updateActorAvatarInstance, updateActorInstance } from '../actor'
|
||||
import {
|
||||
fetchRemoteVideo,
|
||||
generateThumbnailFromUrl,
|
||||
getOrCreateAccountAndVideoAndChannel,
|
||||
getOrCreateVideoChannel,
|
||||
|
@ -51,15 +51,18 @@ function processUpdateVideo (actor: ActorModel, activity: ActivityUpdate) {
|
|||
}
|
||||
|
||||
async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
|
||||
const videoAttributesToUpdate = activity.object as VideoTorrentObject
|
||||
const videoUrl = activity.object.id
|
||||
|
||||
const res = await getOrCreateAccountAndVideoAndChannel(videoAttributesToUpdate.id)
|
||||
const videoObject = await fetchRemoteVideo(videoUrl)
|
||||
if (!videoObject) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
|
||||
|
||||
const res = await getOrCreateAccountAndVideoAndChannel(videoObject.id)
|
||||
|
||||
// Fetch video channel outside the transaction
|
||||
const newVideoChannelActor = await getOrCreateVideoChannel(videoAttributesToUpdate)
|
||||
const newVideoChannelActor = await getOrCreateVideoChannel(videoObject)
|
||||
const newVideoChannel = newVideoChannelActor.VideoChannel
|
||||
|
||||
logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid)
|
||||
logger.debug('Updating remote video "%s".', videoObject.uuid)
|
||||
let videoInstance = res.video
|
||||
let videoFieldsSave: any
|
||||
|
||||
|
@ -77,7 +80,7 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
|
|||
throw new Error('Account ' + actor.url + ' does not own video channel ' + videoChannel.Actor.url)
|
||||
}
|
||||
|
||||
const videoData = await videoActivityObjectToDBAttributes(newVideoChannel, videoAttributesToUpdate, activity.to)
|
||||
const videoData = await videoActivityObjectToDBAttributes(newVideoChannel, videoObject, activity.to)
|
||||
videoInstance.set('name', videoData.name)
|
||||
videoInstance.set('uuid', videoData.uuid)
|
||||
videoInstance.set('url', videoData.url)
|
||||
|
@ -88,6 +91,8 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
|
|||
videoInstance.set('support', videoData.support)
|
||||
videoInstance.set('nsfw', videoData.nsfw)
|
||||
videoInstance.set('commentsEnabled', videoData.commentsEnabled)
|
||||
videoInstance.set('waitTranscoding', videoData.waitTranscoding)
|
||||
videoInstance.set('state', videoData.state)
|
||||
videoInstance.set('duration', videoData.duration)
|
||||
videoInstance.set('createdAt', videoData.createdAt)
|
||||
videoInstance.set('updatedAt', videoData.updatedAt)
|
||||
|
@ -98,8 +103,8 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
|
|||
await videoInstance.save(sequelizeOptions)
|
||||
|
||||
// Don't block on request
|
||||
generateThumbnailFromUrl(videoInstance, videoAttributesToUpdate.icon)
|
||||
.catch(err => logger.warn('Cannot generate thumbnail of %s.', videoAttributesToUpdate.id, { err }))
|
||||
generateThumbnailFromUrl(videoInstance, videoObject.icon)
|
||||
.catch(err => logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err }))
|
||||
|
||||
// Remove old video files
|
||||
const videoFileDestroyTasks: Bluebird<void>[] = []
|
||||
|
@ -108,16 +113,16 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
|
|||
}
|
||||
await Promise.all(videoFileDestroyTasks)
|
||||
|
||||
const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoInstance, videoAttributesToUpdate)
|
||||
const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoInstance, videoObject)
|
||||
const tasks = videoFileAttributes.map(f => VideoFileModel.create(f))
|
||||
await Promise.all(tasks)
|
||||
|
||||
const tags = videoAttributesToUpdate.tag.map(t => t.name)
|
||||
const tags = videoObject.tag.map(t => t.name)
|
||||
const tagInstances = await TagModel.findOrCreateTags(tags, t)
|
||||
await videoInstance.$set('Tags', tagInstances, sequelizeOptions)
|
||||
})
|
||||
|
||||
logger.info('Remote video with uuid %s updated', videoAttributesToUpdate.uuid)
|
||||
logger.info('Remote video with uuid %s updated', videoObject.uuid)
|
||||
} catch (err) {
|
||||
if (videoInstance !== undefined && videoFieldsSave !== undefined) {
|
||||
resetSequelizeInstance(videoInstance, videoFieldsSave)
|
||||
|
|
|
@ -11,7 +11,7 @@ async function buildVideoAnnounce (byActor: ActorModel, videoShare: VideoShareMo
|
|||
|
||||
const accountsToForwardView = await getActorsInvolvedInVideo(video, t)
|
||||
const audience = getObjectFollowersAudience(accountsToForwardView)
|
||||
return announceActivityData(videoShare.url, byActor, announcedObject, t, audience)
|
||||
return announceActivityData(videoShare.url, byActor, announcedObject, audience)
|
||||
}
|
||||
|
||||
async function sendVideoAnnounce (byActor: ActorModel, videoShare: VideoShareModel, video: VideoModel, t: Transaction) {
|
||||
|
@ -20,16 +20,8 @@ async function sendVideoAnnounce (byActor: ActorModel, videoShare: VideoShareMod
|
|||
return broadcastToFollowers(data, byActor, [ byActor ], t)
|
||||
}
|
||||
|
||||
async function announceActivityData (
|
||||
url: string,
|
||||
byActor: ActorModel,
|
||||
object: string,
|
||||
t: Transaction,
|
||||
audience?: ActivityAudience
|
||||
): Promise<ActivityAnnounce> {
|
||||
if (!audience) {
|
||||
audience = await getAudience(byActor, t)
|
||||
}
|
||||
function announceActivityData (url: string, byActor: ActorModel, object: string, audience?: ActivityAudience): ActivityAnnounce {
|
||||
if (!audience) audience = getAudience(byActor)
|
||||
|
||||
return {
|
||||
type: 'Announce',
|
||||
|
|
|
@ -23,8 +23,8 @@ async function sendCreateVideo (video: VideoModel, t: Transaction) {
|
|||
const byActor = video.VideoChannel.Account.Actor
|
||||
const videoObject = video.toActivityPubObject()
|
||||
|
||||
const audience = await getAudience(byActor, t, video.privacy === VideoPrivacy.PUBLIC)
|
||||
const data = await createActivityData(video.url, byActor, videoObject, t, audience)
|
||||
const audience = getAudience(byActor, video.privacy === VideoPrivacy.PUBLIC)
|
||||
const data = createActivityData(video.url, byActor, videoObject, audience)
|
||||
|
||||
return broadcastToFollowers(data, byActor, [ byActor ], t)
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ async function sendVideoAbuse (byActor: ActorModel, videoAbuse: VideoAbuseModel,
|
|||
const url = getVideoAbuseActivityPubUrl(videoAbuse)
|
||||
|
||||
const audience = { to: [ video.VideoChannel.Account.Actor.url ], cc: [] }
|
||||
const data = await createActivityData(url, byActor, videoAbuse.toActivityPubObject(), t, audience)
|
||||
const data = createActivityData(url, byActor, videoAbuse.toActivityPubObject(), audience)
|
||||
|
||||
return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ async function sendCreateVideoComment (comment: VideoCommentModel, t: Transactio
|
|||
audience = getObjectFollowersAudience(actorsInvolvedInComment.concat(parentsCommentActors))
|
||||
}
|
||||
|
||||
const data = await createActivityData(comment.url, byActor, commentObject, t, audience)
|
||||
const data = createActivityData(comment.url, byActor, commentObject, audience)
|
||||
|
||||
// This was a reply, send it to the parent actors
|
||||
const actorsException = [ byActor ]
|
||||
|
@ -82,14 +82,14 @@ async function sendCreateView (byActor: ActorModel, video: VideoModel, t: Transa
|
|||
// Send to origin
|
||||
if (video.isOwned() === false) {
|
||||
const audience = getVideoAudience(video, actorsInvolvedInVideo)
|
||||
const data = await createActivityData(url, byActor, viewActivityData, t, audience)
|
||||
const data = createActivityData(url, byActor, viewActivityData, audience)
|
||||
|
||||
return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
|
||||
}
|
||||
|
||||
// Send to followers
|
||||
const audience = getObjectFollowersAudience(actorsInvolvedInVideo)
|
||||
const data = await createActivityData(url, byActor, viewActivityData, t, audience)
|
||||
const data = createActivityData(url, byActor, viewActivityData, audience)
|
||||
|
||||
// Use the server actor to send the view
|
||||
const serverActor = await getServerActor()
|
||||
|
@ -106,34 +106,31 @@ async function sendCreateDislike (byActor: ActorModel, video: VideoModel, t: Tra
|
|||
// Send to origin
|
||||
if (video.isOwned() === false) {
|
||||
const audience = getVideoAudience(video, actorsInvolvedInVideo)
|
||||
const data = await createActivityData(url, byActor, dislikeActivityData, t, audience)
|
||||
const data = createActivityData(url, byActor, dislikeActivityData, audience)
|
||||
|
||||
return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
|
||||
}
|
||||
|
||||
// Send to followers
|
||||
const audience = getObjectFollowersAudience(actorsInvolvedInVideo)
|
||||
const data = await createActivityData(url, byActor, dislikeActivityData, t, audience)
|
||||
const data = createActivityData(url, byActor, dislikeActivityData, audience)
|
||||
|
||||
const actorsException = [ byActor ]
|
||||
return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, actorsException)
|
||||
}
|
||||
|
||||
async function createActivityData (url: string,
|
||||
byActor: ActorModel,
|
||||
object: any,
|
||||
t: Transaction,
|
||||
audience?: ActivityAudience): Promise<ActivityCreate> {
|
||||
if (!audience) {
|
||||
audience = await getAudience(byActor, t)
|
||||
}
|
||||
function createActivityData (url: string, byActor: ActorModel, object: any, audience?: ActivityAudience): ActivityCreate {
|
||||
if (!audience) audience = getAudience(byActor)
|
||||
|
||||
return audiencify({
|
||||
type: 'Create' as 'Create',
|
||||
id: url + '/activity',
|
||||
actor: byActor.url,
|
||||
object: audiencify(object, audience)
|
||||
}, audience)
|
||||
return audiencify(
|
||||
{
|
||||
type: 'Create' as 'Create',
|
||||
id: url + '/activity',
|
||||
actor: byActor.url,
|
||||
object: audiencify(object, audience)
|
||||
},
|
||||
audience
|
||||
)
|
||||
}
|
||||
|
||||
function createDislikeActivityData (byActor: ActorModel, video: VideoModel) {
|
||||
|
|
|
@ -14,36 +14,31 @@ async function sendLike (byActor: ActorModel, video: VideoModel, t: Transaction)
|
|||
// Send to origin
|
||||
if (video.isOwned() === false) {
|
||||
const audience = getVideoAudience(video, accountsInvolvedInVideo)
|
||||
const data = await likeActivityData(url, byActor, video, t, audience)
|
||||
const data = likeActivityData(url, byActor, video, audience)
|
||||
|
||||
return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
|
||||
}
|
||||
|
||||
// Send to followers
|
||||
const audience = getObjectFollowersAudience(accountsInvolvedInVideo)
|
||||
const data = await likeActivityData(url, byActor, video, t, audience)
|
||||
const data = likeActivityData(url, byActor, video, audience)
|
||||
|
||||
const followersException = [ byActor ]
|
||||
return broadcastToFollowers(data, byActor, accountsInvolvedInVideo, t, followersException)
|
||||
}
|
||||
|
||||
async function likeActivityData (
|
||||
url: string,
|
||||
byActor: ActorModel,
|
||||
video: VideoModel,
|
||||
t: Transaction,
|
||||
audience?: ActivityAudience
|
||||
): Promise<ActivityLike> {
|
||||
if (!audience) {
|
||||
audience = await getAudience(byActor, t)
|
||||
}
|
||||
function likeActivityData (url: string, byActor: ActorModel, video: VideoModel, audience?: ActivityAudience): ActivityLike {
|
||||
if (!audience) audience = getAudience(byActor)
|
||||
|
||||
return audiencify({
|
||||
type: 'Like' as 'Like',
|
||||
id: url,
|
||||
actor: byActor.url,
|
||||
object: video.url
|
||||
}, audience)
|
||||
return audiencify(
|
||||
{
|
||||
type: 'Like' as 'Like',
|
||||
id: url,
|
||||
actor: byActor.url,
|
||||
object: video.url
|
||||
},
|
||||
audience
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -27,7 +27,7 @@ async function sendUndoFollow (actorFollow: ActorFollowModel, t: Transaction) {
|
|||
const undoUrl = getUndoActivityPubUrl(followUrl)
|
||||
|
||||
const object = followActivityData(followUrl, me, following)
|
||||
const data = await undoActivityData(undoUrl, me, object, t)
|
||||
const data = undoActivityData(undoUrl, me, object)
|
||||
|
||||
return unicastTo(data, me, following.inboxUrl)
|
||||
}
|
||||
|
@ -37,18 +37,18 @@ async function sendUndoLike (byActor: ActorModel, video: VideoModel, t: Transact
|
|||
const undoUrl = getUndoActivityPubUrl(likeUrl)
|
||||
|
||||
const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
|
||||
const object = await likeActivityData(likeUrl, byActor, video, t)
|
||||
const object = likeActivityData(likeUrl, byActor, video)
|
||||
|
||||
// Send to origin
|
||||
if (video.isOwned() === false) {
|
||||
const audience = getVideoAudience(video, actorsInvolvedInVideo)
|
||||
const data = await undoActivityData(undoUrl, byActor, object, t, audience)
|
||||
const data = undoActivityData(undoUrl, byActor, object, audience)
|
||||
|
||||
return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
|
||||
}
|
||||
|
||||
const audience = getObjectFollowersAudience(actorsInvolvedInVideo)
|
||||
const data = await undoActivityData(undoUrl, byActor, object, t, audience)
|
||||
const data = undoActivityData(undoUrl, byActor, object, audience)
|
||||
|
||||
const followersException = [ byActor ]
|
||||
return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException)
|
||||
|
@ -60,16 +60,16 @@ async function sendUndoDislike (byActor: ActorModel, video: VideoModel, t: Trans
|
|||
|
||||
const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
|
||||
const dislikeActivity = createDislikeActivityData(byActor, video)
|
||||
const object = await createActivityData(dislikeUrl, byActor, dislikeActivity, t)
|
||||
const object = createActivityData(dislikeUrl, byActor, dislikeActivity)
|
||||
|
||||
if (video.isOwned() === false) {
|
||||
const audience = getVideoAudience(video, actorsInvolvedInVideo)
|
||||
const data = await undoActivityData(undoUrl, byActor, object, t, audience)
|
||||
const data = undoActivityData(undoUrl, byActor, object, audience)
|
||||
|
||||
return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
|
||||
}
|
||||
|
||||
const data = await undoActivityData(undoUrl, byActor, object, t)
|
||||
const data = undoActivityData(undoUrl, byActor, object)
|
||||
|
||||
const followersException = [ byActor ]
|
||||
return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException)
|
||||
|
@ -80,7 +80,7 @@ async function sendUndoAnnounce (byActor: ActorModel, videoShare: VideoShareMode
|
|||
|
||||
const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t)
|
||||
const object = await buildVideoAnnounce(byActor, videoShare, video, t)
|
||||
const data = await undoActivityData(undoUrl, byActor, object, t)
|
||||
const data = undoActivityData(undoUrl, byActor, object)
|
||||
|
||||
const followersException = [ byActor ]
|
||||
return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException)
|
||||
|
@ -97,21 +97,21 @@ export {
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function undoActivityData (
|
||||
function undoActivityData (
|
||||
url: string,
|
||||
byActor: ActorModel,
|
||||
object: ActivityFollow | ActivityLike | ActivityCreate | ActivityAnnounce,
|
||||
t: Transaction,
|
||||
audience?: ActivityAudience
|
||||
): Promise<ActivityUndo> {
|
||||
if (!audience) {
|
||||
audience = await getAudience(byActor, t)
|
||||
}
|
||||
): ActivityUndo {
|
||||
if (!audience) audience = getAudience(byActor)
|
||||
|
||||
return audiencify({
|
||||
type: 'Undo' as 'Undo',
|
||||
id: url,
|
||||
actor: byActor.url,
|
||||
object
|
||||
}, audience)
|
||||
return audiencify(
|
||||
{
|
||||
type: 'Undo' as 'Undo',
|
||||
id: url,
|
||||
actor: byActor.url,
|
||||
object
|
||||
},
|
||||
audience
|
||||
)
|
||||
}
|
||||
|
|
|
@ -15,9 +15,9 @@ async function sendUpdateVideo (video: VideoModel, t: Transaction) {
|
|||
|
||||
const url = getUpdateActivityPubUrl(video.url, video.updatedAt.toISOString())
|
||||
const videoObject = video.toActivityPubObject()
|
||||
const audience = await getAudience(byActor, t, video.privacy === VideoPrivacy.PUBLIC)
|
||||
const audience = getAudience(byActor, video.privacy === VideoPrivacy.PUBLIC)
|
||||
|
||||
const data = await updateActivityData(url, byActor, videoObject, t, audience)
|
||||
const data = updateActivityData(url, byActor, videoObject, audience)
|
||||
|
||||
const actorsInvolved = await VideoShareModel.loadActorsByShare(video.id, t)
|
||||
actorsInvolved.push(byActor)
|
||||
|
@ -30,8 +30,8 @@ async function sendUpdateActor (accountOrChannel: AccountModel | VideoChannelMod
|
|||
|
||||
const url = getUpdateActivityPubUrl(byActor.url, byActor.updatedAt.toISOString())
|
||||
const accountOrChannelObject = accountOrChannel.toActivityPubObject()
|
||||
const audience = await getAudience(byActor, t)
|
||||
const data = await updateActivityData(url, byActor, accountOrChannelObject, t, audience)
|
||||
const audience = getAudience(byActor)
|
||||
const data = updateActivityData(url, byActor, accountOrChannelObject, audience)
|
||||
|
||||
let actorsInvolved: ActorModel[]
|
||||
if (accountOrChannel instanceof AccountModel) {
|
||||
|
@ -56,21 +56,17 @@ export {
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function updateActivityData (
|
||||
url: string,
|
||||
byActor: ActorModel,
|
||||
object: any,
|
||||
t: Transaction,
|
||||
audience?: ActivityAudience
|
||||
): Promise<ActivityUpdate> {
|
||||
if (!audience) {
|
||||
audience = await getAudience(byActor, t)
|
||||
}
|
||||
function updateActivityData (url: string, byActor: ActorModel, object: any, audience?: ActivityAudience): ActivityUpdate {
|
||||
if (!audience) audience = getAudience(byActor)
|
||||
|
||||
return audiencify({
|
||||
type: 'Update' as 'Update',
|
||||
id: url,
|
||||
actor: byActor.url,
|
||||
object: audiencify(object, audience)
|
||||
}, audience)
|
||||
return audiencify(
|
||||
{
|
||||
type: 'Update' as 'Update',
|
||||
id: url,
|
||||
actor: byActor.url,
|
||||
object: audiencify(object, audience
|
||||
)
|
||||
},
|
||||
audience
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import * as Bluebird from 'bluebird'
|
||||
import * as sequelize from 'sequelize'
|
||||
import * as magnetUtil from 'magnet-uri'
|
||||
import { join } from 'path'
|
||||
import * as request from 'request'
|
||||
import { ActivityIconObject } from '../../../shared/index'
|
||||
import { ActivityIconObject, VideoState } from '../../../shared/index'
|
||||
import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
|
||||
import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos'
|
||||
import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos'
|
||||
|
@ -21,6 +22,21 @@ import { VideoShareModel } from '../../models/video/video-share'
|
|||
import { getOrCreateActorAndServerAndModel } from './actor'
|
||||
import { addVideoComments } from './video-comments'
|
||||
import { crawlCollectionPage } from './crawl'
|
||||
import { sendCreateVideo, sendUpdateVideo } from './send'
|
||||
import { shareVideoByServerAndChannel } from './index'
|
||||
|
||||
async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
|
||||
// If the video is not private and published, we federate it
|
||||
if (video.privacy !== VideoPrivacy.PRIVATE && video.state === VideoState.PUBLISHED) {
|
||||
if (isNewVideo === true) {
|
||||
// Now we'll add the video's meta data to our followers
|
||||
await sendCreateVideo(video, transaction)
|
||||
await shareVideoByServerAndChannel(video, transaction)
|
||||
} else {
|
||||
await sendUpdateVideo(video, transaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fetchRemoteVideoPreview (video: VideoModel, reject: Function) {
|
||||
const host = video.VideoChannel.Account.Actor.Server.host
|
||||
|
@ -55,9 +71,11 @@ function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject)
|
|||
return doRequestAndSaveToFile(options, thumbnailPath)
|
||||
}
|
||||
|
||||
async function videoActivityObjectToDBAttributes (videoChannel: VideoChannelModel,
|
||||
videoObject: VideoTorrentObject,
|
||||
to: string[] = []) {
|
||||
async function videoActivityObjectToDBAttributes (
|
||||
videoChannel: VideoChannelModel,
|
||||
videoObject: VideoTorrentObject,
|
||||
to: string[] = []
|
||||
) {
|
||||
const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPrivacy.PUBLIC : VideoPrivacy.UNLISTED
|
||||
const duration = videoObject.duration.replace(/[^\d]+/, '')
|
||||
|
||||
|
@ -90,6 +108,8 @@ async function videoActivityObjectToDBAttributes (videoChannel: VideoChannelMode
|
|||
support,
|
||||
nsfw: videoObject.sensitive,
|
||||
commentsEnabled: videoObject.commentsEnabled,
|
||||
waitTranscoding: videoObject.waitTranscoding,
|
||||
state: videoObject.state,
|
||||
channelId: videoChannel.id,
|
||||
duration: parseInt(duration, 10),
|
||||
createdAt: new Date(videoObject.published),
|
||||
|
@ -185,22 +205,20 @@ async function getOrCreateVideo (videoObject: VideoTorrentObject, channelActor:
|
|||
}
|
||||
|
||||
async function getOrCreateAccountAndVideoAndChannel (videoObject: VideoTorrentObject | string, actor?: ActorModel) {
|
||||
if (typeof videoObject === 'string') {
|
||||
const videoUrl = videoObject
|
||||
const videoUrl = typeof videoObject === 'string' ? videoObject : videoObject.id
|
||||
|
||||
const videoFromDatabase = await VideoModel.loadByUrlAndPopulateAccount(videoUrl)
|
||||
if (videoFromDatabase) {
|
||||
return {
|
||||
video: videoFromDatabase,
|
||||
actor: videoFromDatabase.VideoChannel.Account.Actor,
|
||||
channelActor: videoFromDatabase.VideoChannel.Actor
|
||||
}
|
||||
const videoFromDatabase = await VideoModel.loadByUrlAndPopulateAccount(videoUrl)
|
||||
if (videoFromDatabase) {
|
||||
return {
|
||||
video: videoFromDatabase,
|
||||
actor: videoFromDatabase.VideoChannel.Account.Actor,
|
||||
channelActor: videoFromDatabase.VideoChannel.Actor
|
||||
}
|
||||
|
||||
videoObject = await fetchRemoteVideo(videoUrl)
|
||||
if (!videoObject) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
|
||||
}
|
||||
|
||||
videoObject = await fetchRemoteVideo(videoUrl)
|
||||
if (!videoObject) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
|
||||
|
||||
if (!actor) {
|
||||
const actorObj = videoObject.attributedTo.find(a => a.type === 'Person')
|
||||
if (!actorObj) throw new Error('Cannot find associated actor to video ' + videoObject.url)
|
||||
|
@ -291,20 +309,6 @@ async function addVideoShares (shareUrls: string[], instance: VideoModel) {
|
|||
}
|
||||
}
|
||||
|
||||
export {
|
||||
getOrCreateAccountAndVideoAndChannel,
|
||||
fetchRemoteVideoPreview,
|
||||
fetchRemoteVideoDescription,
|
||||
generateThumbnailFromUrl,
|
||||
videoActivityObjectToDBAttributes,
|
||||
videoFileActivityUrlToDBAttributes,
|
||||
getOrCreateVideo,
|
||||
getOrCreateVideoChannel,
|
||||
addVideoShares
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function fetchRemoteVideo (videoUrl: string): Promise<VideoTorrentObject> {
|
||||
const options = {
|
||||
uri: videoUrl,
|
||||
|
@ -324,3 +328,17 @@ async function fetchRemoteVideo (videoUrl: string): Promise<VideoTorrentObject>
|
|||
|
||||
return body
|
||||
}
|
||||
|
||||
export {
|
||||
federateVideoIfNeeded,
|
||||
fetchRemoteVideo,
|
||||
getOrCreateAccountAndVideoAndChannel,
|
||||
fetchRemoteVideoPreview,
|
||||
fetchRemoteVideoDescription,
|
||||
generateThumbnailFromUrl,
|
||||
videoActivityObjectToDBAttributes,
|
||||
videoFileActivityUrlToDBAttributes,
|
||||
getOrCreateVideo,
|
||||
getOrCreateVideoChannel,
|
||||
addVideoShares
|
||||
}
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
import * as kue from 'kue'
|
||||
import { VideoResolution } from '../../../../shared'
|
||||
import { VideoPrivacy } from '../../../../shared/models/videos'
|
||||
import { VideoResolution, VideoState } from '../../../../shared'
|
||||
import { logger } from '../../../helpers/logger'
|
||||
import { computeResolutionsToTranscode } from '../../../helpers/utils'
|
||||
import { sequelizeTypescript } from '../../../initializers'
|
||||
import { VideoModel } from '../../../models/video/video'
|
||||
import { shareVideoByServerAndChannel } from '../../activitypub'
|
||||
import { sendCreateVideo, sendUpdateVideo } from '../../activitypub/send'
|
||||
import { JobQueue } from '../job-queue'
|
||||
import { federateVideoIfNeeded } from '../../activitypub'
|
||||
import { retryTransactionWrapper } from '../../../helpers/database-utils'
|
||||
import { sequelizeTypescript } from '../../../initializers'
|
||||
|
||||
export type VideoFilePayload = {
|
||||
videoUUID: string
|
||||
isNewVideo: boolean
|
||||
isNewVideo?: boolean
|
||||
resolution?: VideoResolution
|
||||
isPortraitMode?: boolean
|
||||
}
|
||||
|
@ -52,10 +51,20 @@ async function processVideoFile (job: kue.Job) {
|
|||
// Transcoding in other resolution
|
||||
if (payload.resolution) {
|
||||
await video.transcodeOriginalVideofile(payload.resolution, payload.isPortraitMode)
|
||||
await onVideoFileTranscoderOrImportSuccess(video)
|
||||
|
||||
const options = {
|
||||
arguments: [ video ],
|
||||
errorMessage: 'Cannot execute onVideoFileTranscoderOrImportSuccess with many retries.'
|
||||
}
|
||||
await retryTransactionWrapper(onVideoFileTranscoderOrImportSuccess, options)
|
||||
} else {
|
||||
await video.optimizeOriginalVideofile()
|
||||
await onVideoFileOptimizerSuccess(video, payload.isNewVideo)
|
||||
|
||||
const options = {
|
||||
arguments: [ video, payload.isNewVideo ],
|
||||
errorMessage: 'Cannot execute onVideoFileOptimizerSuccess with many retries.'
|
||||
}
|
||||
await retryTransactionWrapper(onVideoFileOptimizerSuccess, options)
|
||||
}
|
||||
|
||||
return video
|
||||
|
@ -64,68 +73,70 @@ async function processVideoFile (job: kue.Job) {
|
|||
async function onVideoFileTranscoderOrImportSuccess (video: VideoModel) {
|
||||
if (video === undefined) return undefined
|
||||
|
||||
// Maybe the video changed in database, refresh it
|
||||
const videoDatabase = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(video.uuid)
|
||||
// Video does not exist anymore
|
||||
if (!videoDatabase) return undefined
|
||||
return sequelizeTypescript.transaction(async t => {
|
||||
// Maybe the video changed in database, refresh it
|
||||
let videoDatabase = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(video.uuid, t)
|
||||
// Video does not exist anymore
|
||||
if (!videoDatabase) return undefined
|
||||
|
||||
if (video.privacy !== VideoPrivacy.PRIVATE) {
|
||||
await sendUpdateVideo(video, undefined)
|
||||
}
|
||||
// We transcoded the video file in another format, now we can publish it
|
||||
const oldState = videoDatabase.state
|
||||
videoDatabase.state = VideoState.PUBLISHED
|
||||
videoDatabase = await videoDatabase.save({ transaction: t })
|
||||
|
||||
return undefined
|
||||
// If the video was not published, we consider it is a new one for other instances
|
||||
const isNewVideo = oldState !== VideoState.PUBLISHED
|
||||
await federateVideoIfNeeded(videoDatabase, isNewVideo, t)
|
||||
|
||||
return undefined
|
||||
})
|
||||
}
|
||||
|
||||
async function onVideoFileOptimizerSuccess (video: VideoModel, isNewVideo: boolean) {
|
||||
if (video === undefined) return undefined
|
||||
|
||||
// Maybe the video changed in database, refresh it
|
||||
const videoDatabase = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(video.uuid)
|
||||
// Video does not exist anymore
|
||||
if (!videoDatabase) return undefined
|
||||
// Outside the transaction (IO on disk)
|
||||
const { videoFileResolution } = await video.getOriginalFileResolution()
|
||||
|
||||
if (video.privacy !== VideoPrivacy.PRIVATE) {
|
||||
if (isNewVideo !== false) {
|
||||
// Now we'll add the video's meta data to our followers
|
||||
await sequelizeTypescript.transaction(async t => {
|
||||
await sendCreateVideo(video, t)
|
||||
await shareVideoByServerAndChannel(video, t)
|
||||
})
|
||||
} else {
|
||||
await sendUpdateVideo(video, undefined)
|
||||
}
|
||||
}
|
||||
return sequelizeTypescript.transaction(async t => {
|
||||
// Maybe the video changed in database, refresh it
|
||||
const videoDatabase = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(video.uuid, t)
|
||||
// Video does not exist anymore
|
||||
if (!videoDatabase) return undefined
|
||||
|
||||
const { videoFileResolution } = await videoDatabase.getOriginalFileResolution()
|
||||
// Create transcoding jobs if there are enabled resolutions
|
||||
const resolutionsEnabled = computeResolutionsToTranscode(videoFileResolution)
|
||||
logger.info(
|
||||
'Resolutions computed for video %s and origin file height of %d.', videoDatabase.uuid, videoFileResolution,
|
||||
{ resolutions: resolutionsEnabled }
|
||||
)
|
||||
|
||||
// Create transcoding jobs if there are enabled resolutions
|
||||
const resolutionsEnabled = computeResolutionsToTranscode(videoFileResolution)
|
||||
logger.info(
|
||||
'Resolutions computed for video %s and origin file height of %d.', videoDatabase.uuid, videoFileResolution,
|
||||
{ resolutions: resolutionsEnabled }
|
||||
)
|
||||
if (resolutionsEnabled.length !== 0) {
|
||||
const tasks: Promise<any>[] = []
|
||||
|
||||
if (resolutionsEnabled.length !== 0) {
|
||||
const tasks: Promise<any>[] = []
|
||||
for (const resolution of resolutionsEnabled) {
|
||||
const dataInput = {
|
||||
videoUUID: videoDatabase.uuid,
|
||||
resolution
|
||||
}
|
||||
|
||||
for (const resolution of resolutionsEnabled) {
|
||||
const dataInput = {
|
||||
videoUUID: videoDatabase.uuid,
|
||||
resolution,
|
||||
isNewVideo
|
||||
const p = JobQueue.Instance.createJob({ type: 'video-file', payload: dataInput })
|
||||
tasks.push(p)
|
||||
}
|
||||
|
||||
const p = JobQueue.Instance.createJob({ type: 'video-file', payload: dataInput })
|
||||
tasks.push(p)
|
||||
await Promise.all(tasks)
|
||||
|
||||
logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled })
|
||||
} else {
|
||||
// No transcoding to do, it's now published
|
||||
video.state = VideoState.PUBLISHED
|
||||
video = await video.save({ transaction: t })
|
||||
|
||||
logger.info('No transcoding jobs created for video %s (no resolutions).', video.uuid)
|
||||
}
|
||||
|
||||
await Promise.all(tasks)
|
||||
|
||||
logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled })
|
||||
} else {
|
||||
logger.info('No transcoding jobs created for video %s (no resolutions enabled).')
|
||||
return undefined
|
||||
}
|
||||
return federateVideoIfNeeded(video, isNewVideo, t)
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -79,6 +79,7 @@ class JobQueue {
|
|||
const res = await handlers[ handlerName ](job)
|
||||
return done(null, res)
|
||||
} catch (err) {
|
||||
logger.error('Cannot execute job %d.', job.id, { err })
|
||||
return done(err)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -14,7 +14,7 @@ function cacheRoute (lifetime: number) {
|
|||
|
||||
// Not cached
|
||||
if (!cached) {
|
||||
logger.debug('Not cached result for route %s.', req.originalUrl)
|
||||
logger.debug('No cached results for route %s.', req.originalUrl)
|
||||
|
||||
const sendSave = res.send.bind(res)
|
||||
|
||||
|
|
|
@ -55,8 +55,13 @@ const videosAddValidator = [
|
|||
.customSanitizer(toValueOrNull)
|
||||
.custom(isVideoLanguageValid).withMessage('Should have a valid language'),
|
||||
body('nsfw')
|
||||
.optional()
|
||||
.toBoolean()
|
||||
.custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
|
||||
body('waitTranscoding')
|
||||
.optional()
|
||||
.toBoolean()
|
||||
.custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
|
||||
body('description')
|
||||
.optional()
|
||||
.customSanitizer(toValueOrNull)
|
||||
|
@ -70,6 +75,7 @@ const videosAddValidator = [
|
|||
.customSanitizer(toValueOrNull)
|
||||
.custom(isVideoTagsValid).withMessage('Should have correct tags'),
|
||||
body('commentsEnabled')
|
||||
.optional()
|
||||
.toBoolean()
|
||||
.custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
|
||||
body('privacy')
|
||||
|
@ -149,6 +155,10 @@ const videosUpdateValidator = [
|
|||
.optional()
|
||||
.toBoolean()
|
||||
.custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
|
||||
body('waitTranscoding')
|
||||
.optional()
|
||||
.toBoolean()
|
||||
.custom(isBooleanValid).withMessage('Should have a valid wait transcoding attribute'),
|
||||
body('privacy')
|
||||
.optional()
|
||||
.toInt()
|
||||
|
|
|
@ -25,7 +25,7 @@ import {
|
|||
Table,
|
||||
UpdatedAt
|
||||
} from 'sequelize-typescript'
|
||||
import { VideoPrivacy, VideoResolution } from '../../../shared'
|
||||
import { VideoPrivacy, VideoResolution, VideoState } from '../../../shared'
|
||||
import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
|
||||
import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
|
||||
import { VideoFilter } from '../../../shared/models/videos/video-query.type'
|
||||
|
@ -47,7 +47,7 @@ import {
|
|||
isVideoLanguageValid,
|
||||
isVideoLicenceValid,
|
||||
isVideoNameValid,
|
||||
isVideoPrivacyValid,
|
||||
isVideoPrivacyValid, isVideoStateValid,
|
||||
isVideoSupportValid
|
||||
} from '../../helpers/custom-validators/videos'
|
||||
import { generateImageFromVideoFile, getVideoFileResolution, transcode } from '../../helpers/ffmpeg-utils'
|
||||
|
@ -66,7 +66,7 @@ import {
|
|||
VIDEO_EXT_MIMETYPE,
|
||||
VIDEO_LANGUAGES,
|
||||
VIDEO_LICENCES,
|
||||
VIDEO_PRIVACIES
|
||||
VIDEO_PRIVACIES, VIDEO_STATES
|
||||
} from '../../initializers'
|
||||
import {
|
||||
getVideoCommentsActivityPubUrl,
|
||||
|
@ -93,10 +93,7 @@ enum ScopeNames {
|
|||
AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
|
||||
WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS',
|
||||
WITH_TAGS = 'WITH_TAGS',
|
||||
WITH_FILES = 'WITH_FILES',
|
||||
WITH_SHARES = 'WITH_SHARES',
|
||||
WITH_RATES = 'WITH_RATES',
|
||||
WITH_COMMENTS = 'WITH_COMMENTS'
|
||||
WITH_FILES = 'WITH_FILES'
|
||||
}
|
||||
|
||||
@Scopes({
|
||||
|
@ -183,7 +180,20 @@ enum ScopeNames {
|
|||
')'
|
||||
)
|
||||
},
|
||||
privacy: VideoPrivacy.PUBLIC
|
||||
// Always list public videos
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
// Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding
|
||||
[ Sequelize.Op.or ]: [
|
||||
{
|
||||
state: VideoState.PUBLISHED
|
||||
},
|
||||
{
|
||||
[ Sequelize.Op.and ]: {
|
||||
state: VideoState.TO_TRANSCODE,
|
||||
waitTranscoding: false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
include: [ videoChannelInclude ]
|
||||
}
|
||||
|
@ -272,42 +282,6 @@ enum ScopeNames {
|
|||
required: true
|
||||
}
|
||||
]
|
||||
},
|
||||
[ScopeNames.WITH_SHARES]: {
|
||||
include: [
|
||||
{
|
||||
['separate' as any]: true,
|
||||
model: () => VideoShareModel.unscoped()
|
||||
}
|
||||
]
|
||||
},
|
||||
[ScopeNames.WITH_RATES]: {
|
||||
include: [
|
||||
{
|
||||
['separate' as any]: true,
|
||||
model: () => AccountVideoRateModel,
|
||||
include: [
|
||||
{
|
||||
model: () => AccountModel.unscoped(),
|
||||
required: true,
|
||||
include: [
|
||||
{
|
||||
attributes: [ 'url' ],
|
||||
model: () => ActorModel.unscoped()
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
[ScopeNames.WITH_COMMENTS]: {
|
||||
include: [
|
||||
{
|
||||
['separate' as any]: true,
|
||||
model: () => VideoCommentModel.unscoped()
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
@Table({
|
||||
|
@ -335,7 +309,7 @@ enum ScopeNames {
|
|||
fields: [ 'channelId' ]
|
||||
},
|
||||
{
|
||||
fields: [ 'id', 'privacy' ]
|
||||
fields: [ 'id', 'privacy', 'state', 'waitTranscoding' ]
|
||||
},
|
||||
{
|
||||
fields: [ 'url'],
|
||||
|
@ -435,6 +409,16 @@ export class VideoModel extends Model<VideoModel> {
|
|||
@Column
|
||||
commentsEnabled: boolean
|
||||
|
||||
@AllowNull(false)
|
||||
@Column
|
||||
waitTranscoding: boolean
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(null)
|
||||
@Is('VideoState', value => throwIfNotValid(value, isVideoStateValid, 'state'))
|
||||
@Column
|
||||
state: VideoState
|
||||
|
||||
@CreatedAt
|
||||
createdAt: Date
|
||||
|
||||
|
@ -671,7 +655,7 @@ export class VideoModel extends Model<VideoModel> {
|
|||
})
|
||||
}
|
||||
|
||||
static listAccountVideosForApi (accountId: number, start: number, count: number, sort: string, hideNSFW: boolean, withFiles = false) {
|
||||
static listUserVideosForApi (accountId: number, start: number, count: number, sort: string, hideNSFW: boolean, withFiles = false) {
|
||||
const query: IFindOptions<VideoModel> = {
|
||||
offset: start,
|
||||
limit: count,
|
||||
|
@ -858,12 +842,13 @@ export class VideoModel extends Model<VideoModel> {
|
|||
.findOne(options)
|
||||
}
|
||||
|
||||
static loadByUUIDAndPopulateAccountAndServerAndTags (uuid: string) {
|
||||
static loadByUUIDAndPopulateAccountAndServerAndTags (uuid: string, t?: Sequelize.Transaction) {
|
||||
const options = {
|
||||
order: [ [ 'Tags', 'name', 'ASC' ] ],
|
||||
where: {
|
||||
uuid
|
||||
}
|
||||
},
|
||||
transaction: t
|
||||
}
|
||||
|
||||
return VideoModel
|
||||
|
@ -905,31 +890,23 @@ export class VideoModel extends Model<VideoModel> {
|
|||
}
|
||||
|
||||
private static getCategoryLabel (id: number) {
|
||||
let categoryLabel = VIDEO_CATEGORIES[id]
|
||||
if (!categoryLabel) categoryLabel = 'Misc'
|
||||
|
||||
return categoryLabel
|
||||
return VIDEO_CATEGORIES[id] || 'Misc'
|
||||
}
|
||||
|
||||
private static getLicenceLabel (id: number) {
|
||||
let licenceLabel = VIDEO_LICENCES[id]
|
||||
if (!licenceLabel) licenceLabel = 'Unknown'
|
||||
|
||||
return licenceLabel
|
||||
return VIDEO_LICENCES[id] || 'Unknown'
|
||||
}
|
||||
|
||||
private static getLanguageLabel (id: string) {
|
||||
let languageLabel = VIDEO_LANGUAGES[id]
|
||||
if (!languageLabel) languageLabel = 'Unknown'
|
||||
|
||||
return languageLabel
|
||||
return VIDEO_LANGUAGES[id] || 'Unknown'
|
||||
}
|
||||
|
||||
private static getPrivacyLabel (id: number) {
|
||||
let privacyLabel = VIDEO_PRIVACIES[id]
|
||||
if (!privacyLabel) privacyLabel = 'Unknown'
|
||||
return VIDEO_PRIVACIES[id] || 'Unknown'
|
||||
}
|
||||
|
||||
return privacyLabel
|
||||
private static getStateLabel (id: number) {
|
||||
return VIDEO_STATES[id] || 'Unknown'
|
||||
}
|
||||
|
||||
getOriginalFile () {
|
||||
|
@ -1026,11 +1003,16 @@ export class VideoModel extends Model<VideoModel> {
|
|||
return join(STATIC_PATHS.PREVIEWS, this.getPreviewName())
|
||||
}
|
||||
|
||||
toFormattedJSON (): Video {
|
||||
toFormattedJSON (options?: {
|
||||
additionalAttributes: {
|
||||
state: boolean,
|
||||
waitTranscoding: boolean
|
||||
}
|
||||
}): Video {
|
||||
const formattedAccount = this.VideoChannel.Account.toFormattedJSON()
|
||||
const formattedVideoChannel = this.VideoChannel.toFormattedJSON()
|
||||
|
||||
return {
|
||||
const videoObject: Video = {
|
||||
id: this.id,
|
||||
uuid: this.uuid,
|
||||
name: this.name,
|
||||
|
@ -1082,6 +1064,19 @@ export class VideoModel extends Model<VideoModel> {
|
|||
avatar: formattedVideoChannel.avatar
|
||||
}
|
||||
}
|
||||
|
||||
if (options) {
|
||||
if (options.additionalAttributes.state) {
|
||||
videoObject.state = {
|
||||
id: this.state,
|
||||
label: VideoModel.getStateLabel(this.state)
|
||||
}
|
||||
}
|
||||
|
||||
if (options.additionalAttributes.waitTranscoding) videoObject.waitTranscoding = this.waitTranscoding
|
||||
}
|
||||
|
||||
return videoObject
|
||||
}
|
||||
|
||||
toFormattedDetailsJSON (): VideoDetails {
|
||||
|
@ -1094,6 +1089,11 @@ export class VideoModel extends Model<VideoModel> {
|
|||
account: this.VideoChannel.Account.toFormattedJSON(),
|
||||
tags: map(this.Tags, 'name'),
|
||||
commentsEnabled: this.commentsEnabled,
|
||||
waitTranscoding: this.waitTranscoding,
|
||||
state: {
|
||||
id: this.state,
|
||||
label: VideoModel.getStateLabel(this.state)
|
||||
},
|
||||
files: []
|
||||
}
|
||||
|
||||
|
@ -1207,6 +1207,8 @@ export class VideoModel extends Model<VideoModel> {
|
|||
language,
|
||||
views: this.views,
|
||||
sensitive: this.nsfw,
|
||||
waitTranscoding: this.waitTranscoding,
|
||||
state: this.state,
|
||||
commentsEnabled: this.commentsEnabled,
|
||||
published: this.publishedAt.toISOString(),
|
||||
updated: this.updatedAt.toISOString(),
|
||||
|
|
|
@ -175,6 +175,7 @@ describe('Test videos API validator', function () {
|
|||
language: 'pt',
|
||||
nsfw: false,
|
||||
commentsEnabled: true,
|
||||
waitTranscoding: true,
|
||||
description: 'my super description',
|
||||
support: 'my super support text',
|
||||
tags: [ 'tag1', 'tag2' ],
|
||||
|
@ -224,20 +225,6 @@ describe('Test videos API validator', function () {
|
|||
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
|
||||
})
|
||||
|
||||
it('Should fail without nsfw attribute', async function () {
|
||||
const fields = omit(baseCorrectParams, 'nsfw')
|
||||
const attaches = baseCorrectAttaches
|
||||
|
||||
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
|
||||
})
|
||||
|
||||
it('Should fail without commentsEnabled attribute', async function () {
|
||||
const fields = omit(baseCorrectParams, 'commentsEnabled')
|
||||
const attaches = baseCorrectAttaches
|
||||
|
||||
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
|
||||
})
|
||||
|
||||
it('Should fail with a long description', async function () {
|
||||
const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(2500) })
|
||||
const attaches = baseCorrectAttaches
|
||||
|
|
|
@ -924,7 +924,7 @@ describe('Test multiple servers', function () {
|
|||
|
||||
describe('With minimum parameters', function () {
|
||||
it('Should upload and propagate the video', async function () {
|
||||
this.timeout(50000)
|
||||
this.timeout(60000)
|
||||
|
||||
const path = '/api/v1/videos/upload'
|
||||
|
||||
|
@ -934,16 +934,14 @@ describe('Test multiple servers', function () {
|
|||
.set('Authorization', 'Bearer ' + servers[1].accessToken)
|
||||
.field('name', 'minimum parameters')
|
||||
.field('privacy', '1')
|
||||
.field('nsfw', 'false')
|
||||
.field('channelId', '1')
|
||||
.field('commentsEnabled', 'true')
|
||||
|
||||
const filePath = join(__dirname, '..', '..', 'fixtures', 'video_short.webm')
|
||||
|
||||
await req.attach('videofile', filePath)
|
||||
.expect(200)
|
||||
|
||||
await wait(25000)
|
||||
await wait(40000)
|
||||
|
||||
for (const server of servers) {
|
||||
const res = await getVideosList(server.url)
|
||||
|
@ -964,7 +962,7 @@ describe('Test multiple servers', function () {
|
|||
},
|
||||
isLocal,
|
||||
duration: 5,
|
||||
commentsEnabled: true,
|
||||
commentsEnabled: false,
|
||||
tags: [ ],
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
channel: {
|
||||
|
|
|
@ -32,7 +32,8 @@ describe('Test services', function () {
|
|||
const oembedUrl = 'http://localhost:9001/videos/watch/' + server.video.uuid
|
||||
|
||||
const res = await getOEmbed(server.url, oembedUrl)
|
||||
const expectedHtml = `<iframe width="560" height="315" src="http://localhost:9001/videos/embed/${server.video.uuid}" ` +
|
||||
const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' +
|
||||
`src="http://localhost:9001/videos/embed/${server.video.uuid}" ` +
|
||||
'frameborder="0" allowfullscreen></iframe>'
|
||||
const expectedThumbnailUrl = 'http://localhost:9001/static/previews/' + server.video.uuid + '.jpg'
|
||||
|
||||
|
|
|
@ -2,11 +2,22 @@
|
|||
|
||||
import * as chai from 'chai'
|
||||
import 'mocha'
|
||||
import { VideoDetails } from '../../../../shared/models/videos'
|
||||
import { VideoDetails, VideoState } from '../../../../shared/models/videos'
|
||||
import { getVideoFileFPS } from '../../../helpers/ffmpeg-utils'
|
||||
import {
|
||||
flushAndRunMultipleServers, flushTests, getVideo, getVideosList, killallServers, root, ServerInfo, setAccessTokensToServers, uploadVideo,
|
||||
wait, webtorrentAdd
|
||||
doubleFollow,
|
||||
flushAndRunMultipleServers,
|
||||
flushTests,
|
||||
getMyVideos,
|
||||
getVideo,
|
||||
getVideosList,
|
||||
killallServers,
|
||||
root,
|
||||
ServerInfo,
|
||||
setAccessTokensToServers,
|
||||
uploadVideo,
|
||||
wait,
|
||||
webtorrentAdd
|
||||
} from '../../utils'
|
||||
import { join } from 'path'
|
||||
|
||||
|
@ -109,6 +120,63 @@ describe('Test video transcoding', function () {
|
|||
}
|
||||
})
|
||||
|
||||
it('Should wait transcoding before publishing the video', async function () {
|
||||
this.timeout(80000)
|
||||
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
|
||||
await wait(15000)
|
||||
|
||||
{
|
||||
// Upload the video, but wait transcoding
|
||||
const videoAttributes = {
|
||||
name: 'waiting video',
|
||||
fixture: 'video_short1.webm',
|
||||
waitTranscoding: true
|
||||
}
|
||||
const resVideo = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, videoAttributes)
|
||||
const videoId = resVideo.body.video.uuid
|
||||
|
||||
// Should be in transcode state
|
||||
const { body } = await getVideo(servers[ 1 ].url, videoId)
|
||||
expect(body.name).to.equal('waiting video')
|
||||
expect(body.state.id).to.equal(VideoState.TO_TRANSCODE)
|
||||
expect(body.state.label).to.equal('To transcode')
|
||||
expect(body.waitTranscoding).to.be.true
|
||||
|
||||
// Should have my video
|
||||
const resMyVideos = await getMyVideos(servers[1].url, servers[1].accessToken, 0, 10)
|
||||
const videoToFindInMine = resMyVideos.body.data.find(v => v.name === 'waiting video')
|
||||
expect(videoToFindInMine).not.to.be.undefined
|
||||
expect(videoToFindInMine.state.id).to.equal(VideoState.TO_TRANSCODE)
|
||||
expect(videoToFindInMine.state.label).to.equal('To transcode')
|
||||
expect(videoToFindInMine.waitTranscoding).to.be.true
|
||||
|
||||
// Should not list this video
|
||||
const resVideos = await getVideosList(servers[1].url)
|
||||
const videoToFindInList = resVideos.body.data.find(v => v.name === 'waiting video')
|
||||
expect(videoToFindInList).to.be.undefined
|
||||
|
||||
// Server 1 should not have the video yet
|
||||
await getVideo(servers[0].url, videoId, 404)
|
||||
}
|
||||
|
||||
await wait(30000)
|
||||
|
||||
for (const server of servers) {
|
||||
const res = await getVideosList(server.url)
|
||||
const videoToFind = res.body.data.find(v => v.name === 'waiting video')
|
||||
expect(videoToFind).not.to.be.undefined
|
||||
|
||||
const res2 = await getVideo(server.url, videoToFind.id)
|
||||
const videoDetails: VideoDetails = res2.body
|
||||
|
||||
expect(videoDetails.state.id).to.equal(VideoState.PUBLISHED)
|
||||
expect(videoDetails.state.label).to.equal('Published')
|
||||
expect(videoDetails.waitTranscoding).to.be.true
|
||||
}
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
killallServers(servers)
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ describe('Test create transcoding jobs', function () {
|
|||
const env = getEnvCli(servers[0])
|
||||
await execCLI(`${env} npm run create-transcoding-job -- -v ${video2UUID}`)
|
||||
|
||||
await wait(30000)
|
||||
await wait(40000)
|
||||
|
||||
for (const server of servers) {
|
||||
const res = await getVideosList(server.url)
|
||||
|
|
|
@ -27,6 +27,7 @@ type VideoAttributes = {
|
|||
language?: string
|
||||
nsfw?: boolean
|
||||
commentsEnabled?: boolean
|
||||
waitTranscoding?: boolean
|
||||
description?: string
|
||||
tags?: string[]
|
||||
channelId?: number
|
||||
|
@ -326,6 +327,7 @@ async function uploadVideo (url: string, accessToken: string, videoAttributesArg
|
|||
language: 'zh',
|
||||
channelId: defaultChannelId,
|
||||
nsfw: true,
|
||||
waitTranscoding: false,
|
||||
description: 'my super description',
|
||||
support: 'my super support text',
|
||||
tags: [ 'tag' ],
|
||||
|
@ -341,6 +343,7 @@ async function uploadVideo (url: string, accessToken: string, videoAttributesArg
|
|||
.field('name', attributes.name)
|
||||
.field('nsfw', JSON.stringify(attributes.nsfw))
|
||||
.field('commentsEnabled', JSON.stringify(attributes.commentsEnabled))
|
||||
.field('waitTranscoding', JSON.stringify(attributes.waitTranscoding))
|
||||
.field('privacy', attributes.privacy.toString())
|
||||
.field('channelId', attributes.channelId)
|
||||
|
||||
|
|
|
@ -176,6 +176,7 @@ async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, languag
|
|||
licence,
|
||||
language,
|
||||
nsfw: isNSFW(videoInfo),
|
||||
waitTranscoding: true,
|
||||
commentsEnabled: true,
|
||||
description: videoInfo.description || undefined,
|
||||
support: undefined,
|
||||
|
|
|
@ -84,6 +84,7 @@ async function run () {
|
|||
fixture: program['file'],
|
||||
thumbnailfile: program['thumbnailPath'],
|
||||
previewfile: program['previewPath'],
|
||||
waitTranscoding: true,
|
||||
privacy: program['privacy'],
|
||||
support: undefined
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
ActivityUrlObject
|
||||
} from './common-objects'
|
||||
import { ActivityPubOrderedCollection } from '../activitypub-ordered-collection'
|
||||
import { VideoState } from '../../videos'
|
||||
|
||||
export interface VideoTorrentObject {
|
||||
type: 'Video'
|
||||
|
@ -19,6 +20,8 @@ export interface VideoTorrentObject {
|
|||
views: number
|
||||
sensitive: boolean
|
||||
commentsEnabled: boolean
|
||||
waitTranscoding: boolean
|
||||
state: VideoState
|
||||
published: string
|
||||
updated: string
|
||||
mediaType: 'text/markdown'
|
||||
|
|
|
@ -13,3 +13,4 @@ export * from './video-rate.type'
|
|||
export * from './video-resolution.enum'
|
||||
export * from './video-update.model'
|
||||
export * from './video.model'
|
||||
export * from './video-state.enum'
|
||||
|
|
|
@ -7,7 +7,8 @@ export interface VideoCreate {
|
|||
description?: string
|
||||
support?: string
|
||||
channelId: number
|
||||
nsfw: boolean
|
||||
nsfw?: boolean
|
||||
waitTranscoding?: boolean
|
||||
name: string
|
||||
tags?: string[]
|
||||
commentsEnabled?: boolean
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
export enum VideoState {
|
||||
PUBLISHED = 1,
|
||||
TO_TRANSCODE = 2
|
||||
}
|
|
@ -11,6 +11,7 @@ export interface VideoUpdate {
|
|||
tags?: string[]
|
||||
commentsEnabled?: boolean
|
||||
nsfw?: boolean
|
||||
waitTranscoding?: boolean
|
||||
channelId?: number
|
||||
thumbnailfile?: Blob
|
||||
previewfile?: Blob
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { VideoResolution } from '../../index'
|
||||
import { VideoResolution, VideoState } from '../../index'
|
||||
import { Account } from '../actors'
|
||||
import { Avatar } from '../avatars/avatar.model'
|
||||
import { VideoChannel } from './video-channel.model'
|
||||
|
@ -41,6 +41,9 @@ export interface Video {
|
|||
dislikes: number
|
||||
nsfw: boolean
|
||||
|
||||
waitTranscoding?: boolean
|
||||
state?: VideoConstant<VideoState>
|
||||
|
||||
account: {
|
||||
id: number
|
||||
uuid: string
|
||||
|
@ -70,4 +73,8 @@ export interface VideoDetails extends Video {
|
|||
files: VideoFile[]
|
||||
account: Account
|
||||
commentsEnabled: boolean
|
||||
|
||||
// Not optional in details (unlike in Video)
|
||||
waitTranscoding: boolean
|
||||
state: VideoConstant<VideoState>
|
||||
}
|
||||
|
|
|
@ -3435,6 +3435,19 @@
|
|||
<p>Video description</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prop-row prop-group">
|
||||
<div class="prop-name">
|
||||
<div class="prop-title">waitTranscoding</div>
|
||||
<div class="prop-subtitle"> in formData </div>
|
||||
<div class="prop-subtitle">
|
||||
<span class="json-property-type">boolean</span>
|
||||
<span class="json-property-range" title="Value limits"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prop-value">
|
||||
<p>Whether or not we wait transcoding before publish the video</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prop-row prop-group">
|
||||
<div class="prop-name">
|
||||
<div class="prop-title">support</div>
|
||||
|
@ -4009,6 +4022,19 @@
|
|||
<p>Video category</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prop-row prop-group">
|
||||
<div class="prop-name">
|
||||
<div class="prop-title">waitTranscoding</div>
|
||||
<div class="prop-subtitle"> in formData </div>
|
||||
<div class="prop-subtitle">
|
||||
<span class="json-property-type">boolean</span>
|
||||
<span class="json-property-range" title="Value limits"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prop-value">
|
||||
<p>Whether or not we wait transcoding before publish the video</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="prop-row prop-group">
|
||||
<div class="prop-name">
|
||||
<div class="prop-title">licence</div>
|
||||
|
|
|
@ -682,6 +682,10 @@ paths:
|
|||
in: formData
|
||||
type: string
|
||||
description: 'Video description'
|
||||
- name: waitTranscoding
|
||||
in: formData
|
||||
type: boolean
|
||||
description: 'Whether or not we wait transcoding before publish the video'
|
||||
- name: support
|
||||
in: formData
|
||||
type: string
|
||||
|
@ -814,6 +818,10 @@ paths:
|
|||
in: formData
|
||||
type: number
|
||||
description: 'Video category'
|
||||
- name: waitTranscoding
|
||||
in: formData
|
||||
type: boolean
|
||||
description: 'Whether or not we wait transcoding before publish the video'
|
||||
- name: licence
|
||||
in: formData
|
||||
type: number
|
||||
|
|
|
@ -63,13 +63,18 @@ $ node dist/server/tools/import-videos.js \
|
|||
* Vimeo: https://vimeo.com/xxxxxx
|
||||
* Dailymotion: https://www.dailymotion.com/xxxxx
|
||||
|
||||
The script will get all public videos from Youtube, download them and upload to PeerTube.
|
||||
Already downloaded videos will not be uploaded twice, so you can run and re-run the script in case of crash, disconnection...
|
||||
The script will get all public videos from Youtube, download them and upload to PeerTube.
|
||||
Already downloaded videos will not be uploaded twice, so you can run and re-run the script in case of crash, disconnection...
|
||||
|
||||
Videos will be publicly available after transcoding (you can see them before that in your account on the web interface).
|
||||
|
||||
|
||||
### upload.js
|
||||
|
||||
You can use this script to import videos directly from the CLI.
|
||||
|
||||
Videos will be publicly available after transcoding (you can see them before that in your account on the web interface).
|
||||
|
||||
```
|
||||
$ cd ${CLONE}
|
||||
$ node dist/server/tools/upload.js --help
|
||||
|
|
Loading…
Reference in New Issue