Add ability to schedule video publication

This commit is contained in:
Chocobozzz 2018-06-15 16:52:15 +02:00
parent 2baea0c77c
commit bbe0f0645c
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
51 changed files with 751 additions and 246 deletions

View File

@ -85,7 +85,7 @@
"ngx-pipes": "^2.1.7",
"node-sass": "^4.1.1",
"npm-font-source-sans-pro": "^1.0.2",
"primeng": "^5.2.6",
"primeng": "^6.0.0-rc.1",
"protractor": "^5.3.2",
"purify-css": "^1.2.5",
"purifycss-webpack": "^0.7.0",

View File

@ -12,6 +12,7 @@ import { AccountService } from '@app/shared/account/account.service'
import { tap } from 'rxjs/operators'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { Subscription } from 'rxjs'
import { ScreenService } from '@app/shared/misc/screen.service'
@Component({
selector: 'my-account-videos',
@ -37,6 +38,7 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
protected notificationsService: NotificationsService,
protected confirmService: ConfirmService,
protected location: Location,
protected screenService: ScreenService,
protected i18n: I18n,
private accountService: AccountService,
private videoService: VideoService

View File

@ -18,7 +18,7 @@
<div class="video-info">
<a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
<span i18n class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>
<div class="video-info-private">{{ video.privacy.label }} - {{ getStateLabel(video) }}</div>
<div class="video-info-private">{{ video.privacy.label }}{{ getStateLabel(video) }}</div>
</div>
<!-- Display only once -->
@ -28,9 +28,9 @@
Cancel
</span>
<span i18n class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()">
<span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()">
<span class="icon icon-delete-white"></span>
Delete
<ng-container i18n>Delete</ng-container>
</span>
</div>
</div>

View File

@ -75,6 +75,7 @@
color: #000;
display: block;
width: fit-content;
font-size: 16px;
font-weight: $font-semibold;
}

View File

@ -1,6 +1,6 @@
import { from as observableFrom, Observable } from 'rxjs'
import { concatAll, tap } from 'rxjs/operators'
import { Component, OnDestroy, OnInit } from '@angular/core'
import { Component, OnDestroy, OnInit, Inject, LOCALE_ID } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { Location } from '@angular/common'
import { immutableAssign } from '@app/shared/misc/utils'
@ -12,7 +12,8 @@ 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'
import { VideoPrivacy, VideoState } from '../../../../../shared/models/videos'
import { ScreenService } from '@app/shared/misc/screen.service'
@Component({
selector: 'my-account-videos',
@ -39,8 +40,10 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
protected notificationsService: NotificationsService,
protected confirmService: ConfirmService,
protected location: Location,
protected screenService: ScreenService,
protected i18n: I18n,
private videoService: VideoService
private videoService: VideoService,
@Inject(LOCALE_ID) private localeId: string
) {
super()
@ -131,12 +134,22 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
}
getStateLabel (video: Video) {
if (video.state.id === VideoState.PUBLISHED) return this.i18n('Published')
let suffix: string
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')
if (video.privacy.id !== VideoPrivacy.PRIVATE && video.state.id === VideoState.PUBLISHED) {
suffix = this.i18n('Published')
} else if (video.scheduledUpdate) {
const updateAt = new Date(video.scheduledUpdate.updateAt.toString()).toLocaleString(this.localeId)
suffix = this.i18n('Publication scheduled on ') + updateAt
} else if (video.state.id === VideoState.TO_TRANSCODE && video.waitTranscoding === true) {
suffix = this.i18n('Waiting transcoding')
} else if (video.state.id === VideoState.TO_TRANSCODE) {
suffix = this.i18n('To transcode')
} else {
return ''
}
return this.i18n('Unknown state')
return ' - ' + suffix
}
protected buildVideoHeight () {

View File

@ -12,6 +12,7 @@ import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
import { tap } from 'rxjs/operators'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { Subscription } from 'rxjs'
import { ScreenService } from '@app/shared/misc/screen.service'
@Component({
selector: 'my-video-channel-videos',
@ -37,6 +38,7 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
protected notificationsService: NotificationsService,
protected confirmService: ConfirmService,
protected location: Location,
protected screenService: ScreenService,
protected i18n: I18n,
private videoChannelService: VideoChannelService,
private videoService: VideoService

View File

@ -2,8 +2,8 @@ import { Component, OnInit } from '@angular/core'
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
import { GuardsCheckStart, NavigationEnd, Router } from '@angular/router'
import { AuthService, RedirectService, ServerService } from '@app/core'
import { isInSmallView } from '@app/shared/misc/utils'
import { is18nPath } from '../../../shared/models/i18n'
import { ScreenService } from '@app/shared/misc/screen.service'
@Component({
selector: 'my-app',
@ -33,7 +33,8 @@ export class AppComponent implements OnInit {
private authService: AuthService,
private serverService: ServerService,
private domSanitizer: DomSanitizer,
private redirectService: RedirectService
private redirectService: RedirectService,
private screenService: ScreenService
) { }
get serverVersion () {
@ -75,14 +76,14 @@ export class AppComponent implements OnInit {
this.serverService.loadVideoPrivacies()
// Do not display menu on small screens
if (isInSmallView()) {
if (this.screenService.isInSmallView()) {
this.isMenuDisplayed = false
}
this.router.events.subscribe(
e => {
// User clicked on a link in the menu, change the page
if (e instanceof GuardsCheckStart && isInSmallView()) {
if (e instanceof GuardsCheckStart && this.screenService.isInSmallView()) {
this.isMenuDisplayed = false
}
}

View File

@ -141,6 +141,7 @@ export class ServerService {
)
.subscribe(({ data, translations }) => {
Object.keys(data)
.map(dataKey => parseInt(dataKey, 10))
.forEach(dataKey => {
const label = data[ dataKey ]

View File

@ -15,6 +15,7 @@ export class VideoValidatorsService {
readonly VIDEO_DESCRIPTION: BuildFormValidator
readonly VIDEO_TAGS: BuildFormValidator
readonly VIDEO_SUPPORT: BuildFormValidator
readonly VIDEO_SCHEDULE_PUBLICATION_AT: BuildFormValidator
constructor (private i18n: I18n) {
@ -84,5 +85,12 @@ export class VideoValidatorsService {
'maxlength': this.i18n('Video support cannot be more than 500 characters long.')
}
}
this.VIDEO_SCHEDULE_PUBLICATION_AT = {
VALIDATORS: [ ],
MESSAGES: {
'required': this.i18n('A date is required to schedule video update.')
}
}
}
}

View File

@ -1,10 +1,10 @@
import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
import { Component, forwardRef, Input, OnInit } from '@angular/core'
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
import { isInSmallView } from '@app/shared/misc/utils'
import { MarkdownService } from '@app/videos/shared'
import { Subject } from 'rxjs/Subject'
import truncate from 'lodash-es/truncate'
import { ScreenService } from '@app/shared/misc/screen.service'
@Component({
selector: 'my-markdown-textarea',
@ -35,7 +35,10 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
private contentChanged = new Subject<string>()
constructor (private markdownService: MarkdownService) {}
constructor (
private screenService: ScreenService,
private markdownService: MarkdownService
) {}
ngOnInit () {
this.contentChanged
@ -76,7 +79,7 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
}
arePreviewsDisplayed () {
return isInSmallView() === false
return this.screenService.isInSmallView() === false
}
private updatePreviews () {

View File

@ -0,0 +1,94 @@
import { I18n } from '@ngx-translate/i18n-polyfill'
import { Injectable } from '@angular/core'
@Injectable()
export class I18nPrimengCalendarService {
private readonly calendarLocale: any = {}
constructor (private i18n: I18n) {
this.calendarLocale = {
firstDayOfWeek: 0,
dayNames: [
this.i18n('Sunday'),
this.i18n('Monday'),
this.i18n('Tuesday'),
this.i18n('Wednesday'),
this.i18n('Thursday'),
this.i18n('Friday'),
this.i18n('Saturday')
],
dayNamesShort: [
this.i18n({ value: 'Sun', description: 'Day name short' }),
this.i18n({ value: 'Mon', description: 'Day name short' }),
this.i18n({ value: 'Tue', description: 'Day name short' }),
this.i18n({ value: 'Wed', description: 'Day name short' }),
this.i18n({ value: 'Thu', description: 'Day name short' }),
this.i18n({ value: 'Fri', description: 'Day name short' }),
this.i18n({ value: 'Sat', description: 'Day name short' })
],
dayNamesMin: [
this.i18n({ value: 'Su', description: 'Day name min' }),
this.i18n({ value: 'Mo', description: 'Day name min' }),
this.i18n({ value: 'Tu', description: 'Day name min' }),
this.i18n({ value: 'We', description: 'Day name min' }),
this.i18n({ value: 'Th', description: 'Day name min' }),
this.i18n({ value: 'Fr', description: 'Day name min' }),
this.i18n({ value: 'Sa', description: 'Day name min' })
],
monthNames: [
this.i18n('January'),
this.i18n('February'),
this.i18n('March'),
this.i18n('April'),
this.i18n('May'),
this.i18n('June'),
this.i18n('July'),
this.i18n('August'),
this.i18n('September'),
this.i18n('October'),
this.i18n('November'),
this.i18n('December')
],
monthNamesShort: [
this.i18n({ value: 'Jan', description: 'Month name short' }),
this.i18n({ value: 'Feb', description: 'Month name short' }),
this.i18n({ value: 'Mar', description: 'Month name short' }),
this.i18n({ value: 'Apr', description: 'Month name short' }),
this.i18n({ value: 'May', description: 'Month name short' }),
this.i18n({ value: 'Jun', description: 'Month name short' }),
this.i18n({ value: 'Jul', description: 'Month name short' }),
this.i18n({ value: 'Aug', description: 'Month name short' }),
this.i18n({ value: 'Sep', description: 'Month name short' }),
this.i18n({ value: 'Oct', description: 'Month name short' }),
this.i18n({ value: 'Nov', description: 'Month name short' }),
this.i18n({ value: 'Dec', description: 'Month name short' })
],
today: this.i18n('Today'),
clear: this.i18n('Clear')
}
}
getCalendarLocale () {
return this.calendarLocale
}
getTimezone () {
const gmt = new Date().toString().match(/([A-Z]+[\+-][0-9]+)/)[1]
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
return `${timezone} - ${gmt}`
}
getDateFormat () {
return this.i18n({
value: 'yy-mm-dd ',
description: 'Date format in this locale.'
})
}
}

View File

@ -0,0 +1,23 @@
import { Injectable, NgZone } from '@angular/core'
@Injectable()
export class ScreenService {
private windowInnerWidth: number
constructor (private zone: NgZone) {
this.windowInnerWidth = window.innerWidth
// Try to cache a little bit window.innerWidth
this.zone.runOutsideAngular(() => {
setInterval(() => this.windowInnerWidth = window.innerWidth, 500)
})
}
isInSmallView () {
return this.windowInnerWidth < 600
}
isInMobileView () {
return this.windowInnerWidth < 500
}
}

View File

@ -96,26 +96,12 @@ function lineFeedToHtml (obj: object, keyToNormalize: string) {
})
}
// Try to cache a little bit window.innerWidth
let windowInnerWidth = window.innerWidth
setInterval(() => windowInnerWidth = window.innerWidth, 500)
function isInSmallView () {
return windowInnerWidth < 600
}
function isInMobileView () {
return windowInnerWidth < 500
}
export {
objectToUrlEncoded,
getParameterByName,
populateAsyncUserVideoChannels,
getAbsoluteAPIUrl,
dateToHuman,
isInSmallView,
isInMobileView,
immutableAssign,
objectToFormData,
lineFeedToHtml

View File

@ -41,6 +41,8 @@ import {
ResetPasswordValidatorsService,
UserValidatorsService, VideoAbuseValidatorsService, VideoChannelValidatorsService, VideoCommentValidatorsService, VideoValidatorsService
} from '@app/shared/forms'
import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar'
import { ScreenService } from '@app/shared/misc/screen.service'
@NgModule({
imports: [
@ -128,6 +130,9 @@ import {
VideoCommentValidatorsService,
VideoValidatorsService,
I18nPrimengCalendarService,
ScreenService,
I18n
]
})

View File

@ -2,7 +2,6 @@ import { debounceTime } from 'rxjs/operators'
import { ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { Location } from '@angular/common'
import { isInMobileView } from '@app/shared/misc/utils'
import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive'
import { NotificationsService } from 'angular2-notifications'
import { fromEvent, Observable, Subscription } from 'rxjs'
@ -11,6 +10,7 @@ import { ComponentPagination } from '../rest/component-pagination.model'
import { VideoSortField } from './sort-field.type'
import { Video } from './video.model'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { ScreenService } from '@app/shared/misc/screen.service'
export abstract class AbstractVideoList implements OnInit, OnDestroy {
private static LINES_PER_PAGE = 4
@ -41,6 +41,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy {
protected abstract authService: AuthService
protected abstract router: Router
protected abstract route: ActivatedRoute
protected abstract screenService: ScreenService
protected abstract i18n: I18n
protected abstract location: Location
protected abstract currentRoute: string
@ -199,7 +200,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy {
}
private calcPageSizes () {
if (isInMobileView() || this.baseVideoWidth === -1) {
if (this.screenService.isInMobileView() || this.baseVideoWidth === -1) {
this.pagination.itemsPerPage = 5
// Video takes all the width

View File

@ -1,8 +1,11 @@
import { VideoDetails } from './video-details.model'
import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum'
import { VideoUpdate } from '../../../../../shared/models/videos'
import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model'
export class VideoEdit implements VideoUpdate {
static readonly SPECIAL_SCHEDULED_PRIVACY = -1
category: number
licence: number
language: string
@ -21,6 +24,7 @@ export class VideoEdit implements VideoUpdate {
previewUrl: string
uuid?: string
id?: number
scheduleUpdate?: VideoScheduleUpdate
constructor (videoDetails?: VideoDetails) {
if (videoDetails) {
@ -40,6 +44,8 @@ export class VideoEdit implements VideoUpdate {
this.support = videoDetails.support
this.thumbnailUrl = videoDetails.thumbnailUrl
this.previewUrl = videoDetails.previewUrl
this.scheduleUpdate = videoDetails.scheduledUpdate
}
}
@ -47,10 +53,22 @@ export class VideoEdit implements VideoUpdate {
Object.keys(values).forEach((key) => {
this[ key ] = values[ key ]
})
// If schedule publication, the video is private and will be changed to public privacy
if (values['schedulePublicationAt']) {
const updateAt = (values['schedulePublicationAt'] as Date)
updateAt.setSeconds(0)
this.privacy = VideoPrivacy.PRIVATE
this.scheduleUpdate = {
updateAt: updateAt.toISOString(),
privacy: VideoPrivacy.PUBLIC
}
}
}
toJSON () {
return {
toFormPatch () {
const json = {
category: this.category,
licence: this.licence,
language: this.language,
@ -64,5 +82,15 @@ export class VideoEdit implements VideoUpdate {
channelId: this.channelId,
privacy: this.privacy
}
// Special case if we scheduled an update
if (this.scheduleUpdate) {
Object.assign(json, {
privacy: VideoEdit.SPECIAL_SCHEDULED_PRIVACY,
schedulePublicationAt: new Date(this.scheduleUpdate.updateAt.toString())
})
}
return json
}
}

View File

@ -1,6 +1,6 @@
import { Component, Input } from '@angular/core'
import { isInMobileView } from '@app/shared/misc/utils'
import { Video } from './video.model'
import { ScreenService } from '@app/shared/misc/screen.service'
@Component({
selector: 'my-video-thumbnail',
@ -11,10 +11,12 @@ export class VideoThumbnailComponent {
@Input() video: Video
@Input() nsfw = false
constructor (private screenService: ScreenService) {}
getImageUrl () {
if (!this.video) return ''
if (isInMobileView()) {
if (this.screenService.isInMobileView()) {
return this.video.previewUrl
}

View File

@ -6,6 +6,7 @@ import { getAbsoluteAPIUrl } from '../misc/utils'
import { ServerConfig } from '../../../../../shared/models'
import { Actor } from '@app/shared/actor/actor.model'
import { peertubeTranslate } from '@app/shared/i18n/i18n-utils'
import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model'
export class Video implements VideoServerModel {
by: string
@ -38,6 +39,7 @@ export class Video implements VideoServerModel {
waitTranscoding?: boolean
state?: VideoConstant<VideoState>
scheduledUpdate?: VideoScheduleUpdate
account: {
id: number
@ -109,6 +111,7 @@ export class Video implements VideoServerModel {
this.language.label = peertubeTranslate(this.language.label, translations)
this.privacy.label = peertubeTranslate(this.privacy.label, translations)
this.scheduledUpdate = hash.scheduledUpdate
if (this.state) this.state.label = peertubeTranslate(this.state.label, translations)
}

View File

@ -83,7 +83,8 @@ export class VideoService {
waitTranscoding: video.waitTranscoding,
commentsEnabled: video.commentsEnabled,
thumbnailfile: video.thumbnailfile,
previewfile: video.previewfile
previewfile: video.previewfile,
scheduleUpdate: video.scheduleUpdate || undefined
}
const data = objectToFormData(body)

View File

@ -88,6 +88,7 @@
<select id="privacy" formControlName="privacy">
<option></option>
<option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
<option *ngIf="schedulePublicationPossible" [value]="SPECIAL_SCHEDULED_PRIVACY">Scheduled</option>
</select>
</div>
@ -96,11 +97,27 @@
</div>
</div>
<div *ngIf="schedulePublicationEnabled" class="form-group">
<label i18n for="schedulePublicationAt">Schedule publication ({{ calendarTimezone }})</label>
<p-calendar
id="schedulePublicationAt" formControlName="schedulePublicationAt" [dateFormat]="calendarDateFormat"
[locale]="calendarLocale" [minDate]="minScheduledDate" [showTime]="true" [hideOnDateTimeSelect]="true"
>
</p-calendar>
<div *ngIf="formErrors.schedulePublicationAt" class="form-error">
{{ formErrors.schedulePublicationAt }}
</div>
</div>
<div class="form-group form-group-checkbox">
<input type="checkbox" id="nsfw" formControlName="nsfw" />
<label for="nsfw"></label>
<label i18n for="nsfw">This video contains mature or explicit content</label>
<my-help tooltipPlacement="top" helpType="custom" i18n-customHtml customHtml="Some instances do not list NSFW videos by default."></my-help>
<my-help
tooltipPlacement="top" helpType="custom" i18n-customHtml
customHtml="Some instances do not list videos containing mature or explicit content by default."
></my-help>
</div>
<div class="form-group form-group-checkbox">

View File

@ -44,18 +44,6 @@
font-size: 15px;
}
.root-tabset /deep/ > .nav {
margin-left: 15px;
margin-bottom: 15px;
.nav-link {
display: flex !important;
align-items: center;
height: 30px !important;
padding: 0 15px !important;
}
}
.advanced-settings .form-group {
margin-bottom: 20px;
}
@ -98,7 +86,35 @@
}
}
p-calendar {
display: block;
/deep/ {
input,
.ui-calendar {
width: 100%;
}
input {
@include peertube-input-text(100%);
color: #000;
}
}
}
/deep/ {
.root-tabset > .nav {
margin-left: 15px;
margin-bottom: 15px;
.nav-link {
display: flex !important;
align-items: center;
height: 30px !important;
padding: 0 15px !important;
}
}
.ng2-tag-input {
border: none !important;
}

View File

@ -1,5 +1,5 @@
import { Component, Input, OnInit } from '@angular/core'
import { FormGroup, ValidatorFn } from '@angular/forms'
import { FormGroup, ValidatorFn, Validators } from '@angular/forms'
import { ActivatedRoute, Router } from '@angular/router'
import { FormReactiveValidationMessages, VideoValidatorsService } from '@app/shared'
import { NotificationsService } from 'angular2-notifications'
@ -7,6 +7,7 @@ import { ServerService } from '../../../core/server'
import { VideoEdit } from '../../../shared/video/video-edit.model'
import { map } from 'rxjs/operators'
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar'
@Component({
selector: 'my-video-edit',
@ -20,16 +21,26 @@ export class VideoEditComponent implements OnInit {
@Input() validationMessages: FormReactiveValidationMessages = {}
@Input() videoPrivacies = []
@Input() userVideoChannels: { id: number, label: string, support: string }[] = []
@Input() schedulePublicationPossible = true
// So that it can be accessed in the template
readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY
videoCategories = []
videoLicences = []
videoLanguages = []
video: VideoEdit
tagValidators: ValidatorFn[]
tagValidatorsMessages: { [ name: string ]: string }
schedulePublicationEnabled = false
error: string = null
calendarLocale: any = {}
minScheduledDate = new Date()
calendarTimezone: string
calendarDateFormat: string
constructor (
private formValidatorService: FormValidatorService,
@ -37,10 +48,15 @@ export class VideoEditComponent implements OnInit {
private route: ActivatedRoute,
private router: Router,
private notificationsService: NotificationsService,
private serverService: ServerService
private serverService: ServerService,
private i18nPrimengCalendarService: I18nPrimengCalendarService
) {
this.tagValidators = this.videoValidatorsService.VIDEO_TAGS.VALIDATORS
this.tagValidatorsMessages = this.videoValidatorsService.VIDEO_TAGS.MESSAGES
this.calendarLocale = this.i18nPrimengCalendarService.getCalendarLocale()
this.calendarTimezone = this.i18nPrimengCalendarService.getTimezone()
this.calendarDateFormat = this.i18nPrimengCalendarService.getDateFormat()
}
updateForm () {
@ -64,7 +80,8 @@ export class VideoEditComponent implements OnInit {
tags: null,
thumbnailfile: null,
previewfile: null,
support: this.videoValidatorsService.VIDEO_SUPPORT
support: this.videoValidatorsService.VIDEO_SUPPORT,
schedulePublicationAt: this.videoValidatorsService.VIDEO_SCHEDULE_PUBLICATION_AT
}
this.formValidatorService.updateForm(
@ -75,6 +92,52 @@ export class VideoEditComponent implements OnInit {
defaultValues
)
this.trackChannelChange()
this.trackPrivacyChange()
}
ngOnInit () {
this.updateForm()
this.videoCategories = this.serverService.getVideoCategories()
this.videoLicences = this.serverService.getVideoLicences()
this.videoLanguages = this.serverService.getVideoLanguages()
setTimeout(() => this.minScheduledDate = new Date(), 1000 * 60) // Update every minute
}
private trackPrivacyChange () {
// We will update the "support" field depending on the channel
this.form.controls[ 'privacy' ]
.valueChanges
.pipe(map(res => parseInt(res.toString(), 10)))
.subscribe(
newPrivacyId => {
this.schedulePublicationEnabled = newPrivacyId === this.SPECIAL_SCHEDULED_PRIVACY
// Value changed
const scheduleControl = this.form.get('schedulePublicationAt')
const waitTranscodingControl = this.form.get('waitTranscoding')
if (this.schedulePublicationEnabled) {
scheduleControl.setValidators([ Validators.required ])
waitTranscodingControl.disable()
waitTranscodingControl.setValue(false)
} else {
scheduleControl.clearValidators()
waitTranscodingControl.enable()
waitTranscodingControl.setValue(true)
}
scheduleControl.updateValueAndValidity()
waitTranscodingControl.updateValueAndValidity()
}
)
}
private trackChannelChange () {
// We will update the "support" field depending on the channel
this.form.controls[ 'channelId' ]
.valueChanges
@ -108,14 +171,6 @@ export class VideoEditComponent implements OnInit {
)
}
ngOnInit () {
this.updateForm()
this.videoCategories = this.serverService.getVideoCategories()
this.videoLicences = this.serverService.getVideoLicences()
this.videoLanguages = this.serverService.getVideoLanguages()
}
private updateSupportField (support: string) {
return this.form.patchValue({ support: support || '' })
}

View File

@ -4,10 +4,12 @@ import { TagInputModule } from 'ngx-chips'
import { SharedModule } from '../../../shared/'
import { VideoEditComponent } from './video-edit.component'
import { VideoImageComponent } from './video-image.component'
import { CalendarModule } from 'primeng/components/calendar/calendar'
@NgModule({
imports: [
TagInputModule,
CalendarModule,
SharedModule
],
@ -20,6 +22,7 @@ import { VideoImageComponent } from './video-image.component'
exports: [
TagInputModule,
TabsModule,
CalendarModule,
VideoEditComponent
],

View File

@ -27,6 +27,7 @@
<div class="peertube-select-container">
<select id="first-step-privacy" [(ngModel)]="firstStepPrivacyId">
<option *ngFor="let privacy of videoPrivacies" [value]="privacy.id">{{ privacy.label }}</option>
<option [value]="SPECIAL_SCHEDULED_PRIVACY">Scheduled</option>
</select>
</div>
</div>

View File

@ -27,6 +27,9 @@ import { FormValidatorService } from '@app/shared/forms/form-validators/form-val
export class VideoAddComponent extends FormReactive implements OnInit, OnDestroy, CanComponentDeactivate {
@ViewChild('videofileInput') videofileInput
// So that it can be accessed in the template
readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY
isUploadingVideo = false
isUpdatingVideo = false
videoUploaded = false

View File

@ -6,7 +6,7 @@
<form novalidate [formGroup]="form">
<my-video-edit
[form]="form" [formErrors]="formErrors"
[form]="form" [formErrors]="formErrors" [schedulePublicationPossible]="schedulePublicationPossible"
[validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels"
></my-video-edit>

View File

@ -24,6 +24,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
isUpdatingVideo = false
videoPrivacies = []
userVideoChannels = []
schedulePublicationPossible = false
constructor (
protected formValidatorService: FormValidatorService,
@ -70,13 +71,10 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
this.userVideoChannels = videoChannels
// We cannot set private a video that was not private
if (video.privacy.id !== VideoPrivacy.PRIVATE) {
const newVideoPrivacies = []
for (const p of this.videoPrivacies) {
if (p.id !== VideoPrivacy.PRIVATE) newVideoPrivacies.push(p)
}
this.videoPrivacies = newVideoPrivacies
if (this.video.privacy !== VideoPrivacy.PRIVATE) {
this.videoPrivacies = this.videoPrivacies.filter(p => p.id !== VideoPrivacy.PRIVATE)
} else { // We can schedule video publication only if it it is private
this.schedulePublicationPossible = this.video.privacy === VideoPrivacy.PRIVATE
}
this.hydrateFormFromVideo()
@ -123,7 +121,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
}
private hydrateFormFromVideo () {
this.form.patchValue(this.video.toJSON())
this.form.patchValue(this.video.toFormPatch())
const objects = [
{

View File

@ -3,10 +3,14 @@
<div id="video-element-wrapper">
</div>
<div i18n id="warning-transcoding" class="alert alert-warning" *ngIf="isVideoToTranscode()">
<div i18n class="alert alert-warning" *ngIf="isVideoToTranscode()">
The video is being transcoded, it may not work properly.
</div>
<div i18n class="alert alert-info" *ngIf="hasVideoScheduledPublication()">
This video will be published on {{ video.scheduledUpdate.updateAt | date: 'full' }}
</div>
<!-- Video information -->
<div *ngIf="video" class="margin-content video-bottom">
<div class="video-info">

View File

@ -28,7 +28,7 @@
}
}
#warning-transcoding {
.alert {
text-align: center;
}

View File

@ -280,6 +280,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
return this.video && this.video.state.id === VideoState.TO_TRANSCODE
}
hasVideoScheduledPublication () {
return this.video && this.video.scheduledUpdate !== undefined
}
private updateVideoDescription (description: string) {
this.video.description = description
this.setVideoDescriptionHTML()

View File

@ -9,6 +9,7 @@ import { VideoSortField } from '../../shared/video/sort-field.type'
import { VideoService } from '../../shared/video/video.service'
import { VideoFilter } from '../../../../../shared/models/videos/video-query.type'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { ScreenService } from '@app/shared/misc/screen.service'
@Component({
selector: 'my-videos-local',
@ -28,6 +29,7 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On
protected authService: AuthService,
protected location: Location,
protected i18n: I18n,
protected screenService: ScreenService,
private videoService: VideoService
) {
super()

View File

@ -8,6 +8,7 @@ import { AbstractVideoList } from '../../shared/video/abstract-video-list'
import { VideoSortField } from '../../shared/video/sort-field.type'
import { VideoService } from '../../shared/video/video.service'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { ScreenService } from '@app/shared/misc/screen.service'
@Component({
selector: 'my-videos-recently-added',
@ -26,6 +27,7 @@ export class VideoRecentlyAddedComponent extends AbstractVideoList implements On
protected notificationsService: NotificationsService,
protected authService: AuthService,
protected i18n: I18n,
protected screenService: ScreenService,
private videoService: VideoService
) {
super()

View File

@ -9,6 +9,7 @@ import { AuthService } from '../../core/auth'
import { AbstractVideoList } from '../../shared/video/abstract-video-list'
import { VideoService } from '../../shared/video/video.service'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { ScreenService } from '@app/shared/misc/screen.service'
@Component({
selector: 'my-videos-search',
@ -32,6 +33,7 @@ export class VideoSearchComponent extends AbstractVideoList implements OnInit, O
protected authService: AuthService,
protected location: Location,
protected i18n: I18n,
protected screenService: ScreenService,
private videoService: VideoService,
private redirectService: RedirectService
) {

View File

@ -8,6 +8,7 @@ import { AbstractVideoList } from '../../shared/video/abstract-video-list'
import { VideoSortField } from '../../shared/video/sort-field.type'
import { VideoService } from '../../shared/video/video.service'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { ScreenService } from '@app/shared/misc/screen.service'
@Component({
selector: 'my-videos-trending',
@ -25,6 +26,7 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
protected notificationsService: NotificationsService,
protected authService: AuthService,
protected location: Location,
protected screenService: ScreenService,
protected i18n: I18n,
private videoService: VideoService
) {

View File

@ -6,14 +6,14 @@ $icon-font-path: '../../node_modules/bootstrap-sass/assets/fonts/bootstrap/';
@import '_fonts';
@import '~primeng/resources/themes/bootstrap/theme.css';
@import '~primeng/resources/primeng.css';
@import '~video.js/dist/video-js.css';
$assets-path: '../assets/';
@import './player/player';
@import './loading-bar';
@import './primeng-custom';
[hidden] {
display: none !important;
}
@ -142,126 +142,6 @@ label {
to { transform: scale(1) rotate(360deg);}
}
// ngprime data table customizations
p-table {
font-size: 15px !important;
td {
border: 1px solid #E5E5E5 !important;
padding-left: 15px !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
white-space: nowrap !important;
}
tr {
background-color: #fff !important;
height: 46px;
}
.ui-table-tbody {
tr {
&:hover {
background-color: #f0f0f0 !important;
}
&:not(:hover) {
.action-cell * {
display: none !important;
}
}
&:first-child td {
border-top: none !important;
}
&:last-child td {
border-bottom: none !important;
}
}
.expander {
cursor: pointer;
position: relative;
top: 1px;
}
}
th {
border: none !important;
border-bottom: 1px solid #f0f0f0 !important;
text-align: left !important;
padding: 5px 0 5px 15px !important;
font-weight: $font-semibold !important;
color: #000 !important;
&.ui-sortable-column:hover {
background-color: #f0f0f0 !important;
border: 1px solid #f0f0f0 !important;
border-width: 0 1px !important;
&:first-child {
border-width: 0 1px 0 0 !important;
}
}
&.ui-state-highlight {
background-color: #fff !important;
.fa {
@extend .glyphicon;
font-size: 11px;
&.fa-sort-asc {
@extend .glyphicon-triangle-top;
}
&.fa-sort-desc {
@extend .glyphicon-triangle-bottom;
}
}
}
}
.action-cell {
width: 250px !important;
padding: 0 !important;
text-align: center;
my-edit-button + my-delete-button {
margin-left: 5px;
}
}
p-paginator {
.ui-paginator-bottom {
position: relative;
border: none !important;
border: 1px solid #f0f0f0 !important;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
a {
color: #000 !important;
font-weight: $font-semibold !important;
margin-right: 20px !important;
outline: 0 !important;
border-radius: 3px !important;
padding: 5px 2px !important;
&.ui-state-active {
&, &:hover, &:active, &:focus {
color: #fff !important;
background-color: $orange-color !important;
}
}
}
}
}
}
// Bootstrap customizations
.dropdown-menu {
border-radius: 3px;
@ -352,6 +232,8 @@ tabset:not(.bootstrap) {
}
tabset.bootstrap {
margin-left: 0;
.nav-item .nav-link {
&, & a {
color: #000;

View File

@ -281,6 +281,12 @@
cursor: pointer;
display: inline;
}
&[disabled] + label,
&[disabled] + label + label{
opacity: 0.5;
cursor: default;
}
}

View File

@ -0,0 +1,174 @@
@import '_variables';
@import '_mixins';
@import '~primeng/resources/primeng.css';
@import '~primeng/resources/themes/bootstrap/theme.css';
@mixin glyphicon-light {
font-family: 'Glyphicons Halflings';
text-decoration: none !important;
color: #000 !important;
}
// data table customizations
p-table {
font-size: 15px !important;
td {
border: 1px solid #E5E5E5 !important;
padding-left: 15px !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
white-space: nowrap !important;
}
tr {
background-color: #fff !important;
height: 46px;
}
.ui-table-tbody {
tr {
&:hover {
background-color: #f0f0f0 !important;
}
&:not(:hover) {
.action-cell * {
display: none !important;
}
}
&:first-child td {
border-top: none !important;
}
&:last-child td {
border-bottom: none !important;
}
}
.expander {
cursor: pointer;
position: relative;
top: 1px;
}
}
th {
border: none !important;
border-bottom: 1px solid #f0f0f0 !important;
text-align: left !important;
padding: 5px 0 5px 15px !important;
font-weight: $font-semibold !important;
color: #000 !important;
&.ui-sortable-column:hover {
background-color: #f0f0f0 !important;
border: 1px solid #f0f0f0 !important;
border-width: 0 1px !important;
&:first-child {
border-width: 0 1px 0 0 !important;
}
}
&.ui-state-highlight {
background-color: #fff !important;
.pi {
@extend .glyphicon;
color: #000;
font-size: 11px;
&.pi-sort-up {
@extend .glyphicon-triangle-top;
}
&.pi-sort-down {
@extend .glyphicon-triangle-bottom;
}
}
}
}
.action-cell {
width: 250px !important;
padding: 0 !important;
text-align: center;
my-edit-button + my-delete-button {
margin-left: 5px;
}
}
p-paginator {
.ui-paginator-bottom {
position: relative;
border: 1px solid #f0f0f0 !important;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
.ui-paginator-pages {
height: auto !important;
a {
color: #000 !important;
font-weight: $font-semibold !important;
margin-right: 20px !important;
outline: 0 !important;
border-radius: 3px !important;
padding: 5px 2px !important;
height: auto !important;
&.ui-state-active {
&, &:hover, &:active, &:focus {
color: #fff !important;
background-color: $orange-color !important;
}
}
}
}
}
}
}
// PrimeNG calendar tweaks
p-calendar .ui-datepicker {
a {
@include disable-default-a-behaviour;
}
.ui-datepicker-header {
.ui-datepicker-year {
margin-left: 5px;
}
.ui-datepicker-next {
@extend .glyphicon-chevron-right;
@include glyphicon-light;
}
.ui-datepicker-prev {
@extend .glyphicon-chevron-left;
@include glyphicon-light;
}
}
.ui-timepicker {
.pi.pi-chevron-up {
@extend .glyphicon-chevron-up;
@include glyphicon-light;
}
.pi.pi-chevron-down {
@extend .glyphicon-chevron-down;
@include glyphicon-light;
}
}
}

View File

@ -7656,9 +7656,9 @@ pretty-error@^2.0.2:
renderkid "^2.0.1"
utila "~0.4"
primeng@^5.2.6:
version "5.2.7"
resolved "https://registry.yarnpkg.com/primeng/-/primeng-5.2.7.tgz#9dcf461b6a82ea46de85751dc235ea82303e64b1"
primeng@^6.0.0-rc.1:
version "6.0.0-rc.1"
resolved "https://registry.yarnpkg.com/primeng/-/primeng-6.0.0-rc.1.tgz#038e5657a5395e08a5c1fd9312b12cac1a44b527"
private@^0.1.6, private@^0.1.8, private@~0.1.5:
version "0.1.8"

View File

@ -8,8 +8,6 @@ import { VideoPrivacy } from '../../shared/models/videos'
import { buildPath, isTestInstance, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils'
import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
import { invert } from 'lodash'
import { RemoveOldJobsScheduler } from '../lib/schedulers/remove-old-jobs-scheduler'
import { UpdateVideosScheduler } from '../lib/schedulers/update-videos-scheduler'
// Use a variable to reload the configuration if we need
let config: IConfig = require('config')
@ -98,8 +96,8 @@ const JOB_COMPLETED_LIFETIME = 60000 * 60 * 24 * 2 // 2 days
// 1 hour
let SCHEDULER_INTERVALS_MS = {
badActorFollow: 60000 * 60, // 1 hour
removeOldJobs: 60000 * 60, // 1 jour
updateVideos: 60000 * 1, // 1 minute
removeOldJobs: 60000 * 60, // 1 hour
updateVideos: 60000 // 1 minute
}
// ---------------------------------------------------------------------------

View File

@ -33,7 +33,9 @@ export class UpdateVideosScheduler extends AbstractScheduler {
}
}
private updateVideos () {
private async updateVideos () {
if (!await ScheduleVideoUpdateModel.areVideosToUpdate()) return undefined
return sequelizeTypescript.transaction(async t => {
const schedules = await ScheduleVideoUpdateModel.listVideosToUpdate(t)

View File

@ -223,7 +223,7 @@ const videosUpdateValidator = [
if (video.privacy !== VideoPrivacy.PRIVATE && req.body.privacy === VideoPrivacy.PRIVATE) {
return res.status(409)
.json({ error: 'Cannot set "private" a video that was not private anymore.' })
.json({ error: 'Cannot set "private" a video that was not private.' })
.end()
}

View File

@ -25,7 +25,7 @@ export class ScheduleVideoUpdateModel extends Model<ScheduleVideoUpdateModel> {
@AllowNull(true)
@Default(null)
@Column
privacy: VideoPrivacy
privacy: VideoPrivacy.PUBLIC | VideoPrivacy.UNLISTED
@CreatedAt
createdAt: Date
@ -45,6 +45,21 @@ export class ScheduleVideoUpdateModel extends Model<ScheduleVideoUpdateModel> {
})
Video: VideoModel
static areVideosToUpdate () {
const query = {
logging: false,
attributes: [ 'id' ],
where: {
updateAt: {
[Sequelize.Op.lte]: new Date()
}
}
}
return ScheduleVideoUpdateModel.findOne(query)
.then(res => !!res)
}
static listVideosToUpdate (t: Transaction) {
const query = {
where: {
@ -68,4 +83,10 @@ export class ScheduleVideoUpdateModel extends Model<ScheduleVideoUpdateModel> {
return ScheduleVideoUpdateModel.findAll(query)
}
toFormattedJSON () {
return {
updateAt: this.updateAt,
privacy: this.privacy || undefined
}
}
}

View File

@ -97,7 +97,8 @@ export enum ScopeNames {
AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS',
WITH_TAGS = 'WITH_TAGS',
WITH_FILES = 'WITH_FILES'
WITH_FILES = 'WITH_FILES',
WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE'
}
@Scopes({
@ -286,6 +287,14 @@ export enum ScopeNames {
required: true
}
]
},
[ScopeNames.WITH_SCHEDULED_UPDATE]: {
include: [
{
model: () => ScheduleVideoUpdateModel.unscoped(),
required: false
}
]
}
})
@Table({
@ -843,7 +852,7 @@ export class VideoModel extends Model<VideoModel> {
}
return VideoModel
.scope([ ScopeNames.WITH_TAGS, ScopeNames.WITH_FILES, ScopeNames.WITH_ACCOUNT_DETAILS ])
.scope([ ScopeNames.WITH_TAGS, ScopeNames.WITH_FILES, ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_SCHEDULED_UPDATE ])
.findById(id, options)
}
@ -869,7 +878,7 @@ export class VideoModel extends Model<VideoModel> {
}
return VideoModel
.scope([ ScopeNames.WITH_TAGS, ScopeNames.WITH_FILES, ScopeNames.WITH_ACCOUNT_DETAILS ])
.scope([ ScopeNames.WITH_TAGS, ScopeNames.WITH_FILES, ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_SCHEDULED_UPDATE ])
.findOne(options)
}
@ -1022,9 +1031,9 @@ export class VideoModel extends Model<VideoModel> {
toFormattedJSON (options?: {
additionalAttributes: {
state: boolean,
waitTranscoding: boolean,
scheduledUpdate: boolean
state?: boolean,
waitTranscoding?: boolean,
scheduledUpdate?: boolean
}
}): Video {
const formattedAccount = this.VideoChannel.Account.toFormattedJSON()
@ -1084,18 +1093,18 @@ export class VideoModel extends Model<VideoModel> {
}
if (options) {
if (options.additionalAttributes.state) {
if (options.additionalAttributes.state === true) {
videoObject.state = {
id: this.state,
label: VideoModel.getStateLabel(this.state)
}
}
if (options.additionalAttributes.waitTranscoding) {
if (options.additionalAttributes.waitTranscoding === true) {
videoObject.waitTranscoding = this.waitTranscoding
}
if (options.additionalAttributes.scheduledUpdate && this.ScheduleVideoUpdate) {
if (options.additionalAttributes.scheduledUpdate === true && this.ScheduleVideoUpdate) {
videoObject.scheduledUpdate = {
updateAt: this.ScheduleVideoUpdate.updateAt,
privacy: this.ScheduleVideoUpdate.privacy || undefined
@ -1107,7 +1116,11 @@ export class VideoModel extends Model<VideoModel> {
}
toFormattedDetailsJSON (): VideoDetails {
const formattedJson = this.toFormattedJSON()
const formattedJson = this.toFormattedJSON({
additionalAttributes: {
scheduledUpdate: true
}
})
const detailsJson = {
support: this.support,

View File

@ -291,6 +291,23 @@ describe('Test videos API validator', function () {
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
})
it('Should fail with a bad schedule update (miss updateAt)', async function () {
const fields = immutableAssign(baseCorrectParams, { 'scheduleUpdate[privacy]': VideoPrivacy.PUBLIC })
const attaches = baseCorrectAttaches
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
})
it('Should fail with a bad schedule update (wrong updateAt)', async function () {
const fields = immutableAssign(baseCorrectParams, {
'scheduleUpdate[privacy]': VideoPrivacy.PUBLIC,
'scheduleUpdate[updateAt]': 'toto'
})
const attaches = baseCorrectAttaches
await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
})
it('Should fail without an input file', async function () {
const fields = baseCorrectParams
const attaches = {}
@ -494,6 +511,18 @@ describe('Test videos API validator', function () {
await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields })
})
it('Should fail with a bad schedule update (miss updateAt)', async function () {
const fields = immutableAssign(baseCorrectParams, { scheduleUpdate: { privacy: VideoPrivacy.PUBLIC } })
await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields })
})
it('Should fail with a bad schedule update (wrong updateAt)', async function () {
const fields = immutableAssign(baseCorrectParams, { scheduleUpdate: { updateAt: 'toto', privacy: VideoPrivacy.PUBLIC } })
await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields })
})
it('Should fail with an incorrect thumbnail file', async function () {
const fields = baseCorrectParams
const attaches = {

View File

@ -5,11 +5,14 @@ import 'mocha'
import { VideoPrivacy } from '../../../../shared/models/videos'
import {
doubleFollow,
flushAndRunMultipleServers, getMyVideos,
flushAndRunMultipleServers,
getMyVideos,
getVideosList,
getVideoWithToken,
killallServers,
ServerInfo,
setAccessTokensToServers, updateVideo,
setAccessTokensToServers,
updateVideo,
uploadVideo,
wait
} from '../../utils'
@ -69,17 +72,22 @@ describe('Test video update scheduler', function () {
const res = await getMyVideos(servers[0].url, servers[0].accessToken, 0, 5)
expect(res.body.total).to.equal(1)
const video = res.body.data[0]
expect(video.name).to.equal('video 1')
expect(video.privacy.id).to.equal(VideoPrivacy.PRIVATE)
expect(new Date(video.scheduledUpdate.updateAt)).to.be.above(new Date())
expect(video.scheduledUpdate.privacy).to.equal(VideoPrivacy.PUBLIC)
const videoFromList = res.body.data[0]
const res2 = await getVideoWithToken(servers[0].url, servers[0].accessToken, videoFromList.uuid)
const videoFromGet = res2.body
for (const video of [ videoFromList, videoFromGet ]) {
expect(video.name).to.equal('video 1')
expect(video.privacy.id).to.equal(VideoPrivacy.PRIVATE)
expect(new Date(video.scheduledUpdate.updateAt)).to.be.above(new Date())
expect(video.scheduledUpdate.privacy).to.equal(VideoPrivacy.PUBLIC)
}
})
it('Should wait some seconds and have the video in public privacy', async function () {
this.timeout(20000)
await wait(10000)
await wait(15000)
await waitJobs(servers)
for (const server of servers) {
@ -144,7 +152,7 @@ describe('Test video update scheduler', function () {
it('Should wait some seconds and have the updated video in public privacy', async function () {
this.timeout(20000)
await wait(10000)
await wait(15000)
await waitJobs(servers)
for (const server of servers) {

View File

@ -1,4 +1,5 @@
import { VideoPrivacy } from './video-privacy.enum'
import { VideoScheduleUpdate } from './video-schedule-update.model'
export interface VideoCreate {
category?: number
@ -13,8 +14,5 @@ export interface VideoCreate {
tags?: string[]
commentsEnabled?: boolean
privacy: VideoPrivacy
scheduleUpdate?: {
updateAt: Date
privacy?: VideoPrivacy.PUBLIC | VideoPrivacy.UNLISTED
}
scheduleUpdate?: VideoScheduleUpdate
}

View File

@ -0,0 +1,6 @@
import { VideoPrivacy } from './video-privacy.enum'
export interface VideoScheduleUpdate {
updateAt: Date | string
privacy?: VideoPrivacy.PUBLIC | VideoPrivacy.UNLISTED // Cannot schedule an update to PRIVATE
}

View File

@ -1,4 +1,5 @@
import { VideoPrivacy } from './video-privacy.enum'
import { VideoScheduleUpdate } from './video-schedule-update.model'
export interface VideoUpdate {
name?: string
@ -15,8 +16,5 @@ export interface VideoUpdate {
channelId?: number
thumbnailfile?: Blob
previewfile?: Blob
scheduleUpdate?: {
updateAt: Date
privacy?: VideoPrivacy
}
scheduleUpdate?: VideoScheduleUpdate
}

View File

@ -3,6 +3,7 @@ import { Account } from '../actors'
import { Avatar } from '../avatars/avatar.model'
import { VideoChannel } from './video-channel.model'
import { VideoPrivacy } from './video-privacy.enum'
import { VideoScheduleUpdate } from './video-schedule-update.model'
export interface VideoConstant <T> {
id: T
@ -43,10 +44,7 @@ export interface Video {
waitTranscoding?: boolean
state?: VideoConstant<VideoState>
scheduledUpdate?: {
updateAt: Date | string
privacy?: VideoPrivacy
}
scheduledUpdate?: VideoScheduleUpdate
account: {
id: number

View File

@ -264,6 +264,7 @@
<a href="#definition-GetMeVideoRating"> GetMeVideoRating </a>
<a href="#definition-RegisterUser"> RegisterUser </a>
<a href="#definition-VideoChannelInput"> VideoChannelInput </a>
<a href="#definition-ScheduleVideoUpdate"> ScheduleVideoUpdate </a>
</nav>
</div>
<div id="docs" class="row collapse expanded drawer" data-drawer>
@ -3530,6 +3531,19 @@
<p>Video privacy</p>
</div>
</div>
<div class="prop-row prop-group">
<div class="prop-name">
<div class="prop-title">scheduleUpdate</div>
<div class="prop-subtitle"> in formData </div>
<div class="prop-subtitle">
<span class="json-property-type">[object Object]</span>
<span class="json-property-range" title="Value limits"></span>
</div>
</div>
<div class="prop-value">
<p class="no-description">(no description)</p>
</div>
</div>
</section>
</div>
<div class="doc-examples"></div>
@ -4161,12 +4175,7 @@
<span class="json-property-required"></span>
<div class="prop-subtitle"> in formData </div>
<div class="prop-subtitle">
<span class="json-property-type">string</span>
<span class="json-property-enum" title="Possible values">
<span class="json-property-enum-item">Public</span>,
<span class="json-property-enum-item">Unlisted</span>,
<span class="json-property-enum-item">Private</span>
</span>
<span class="json-property-type">[object Object]</span>
<span class="json-property-range" title="Value limits"></span>
</div>
</div>
@ -4174,6 +4183,19 @@
<p>Video privacy</p>
</div>
</div>
<div class="prop-row prop-group">
<div class="prop-name">
<div class="prop-title">scheduleUpdate</div>
<div class="prop-subtitle"> in formData </div>
<div class="prop-subtitle">
<span class="json-property-type">[object Object]</span>
<span class="json-property-range" title="Value limits"></span>
</div>
</div>
<div class="prop-value">
<p class="no-description">(no description)</p>
</div>
</div>
</section>
</div>
<div class="doc-examples"></div>
@ -8339,6 +8361,52 @@
<span class="hljs-attr">&quot;name&quot;</span>: <span class="hljs-string">&quot;string&quot;</span>,
<span class="hljs-attr">&quot;description&quot;</span>: <span class="hljs-string">&quot;string&quot;</span>
}
</code></pre>
<!-- </div> -->
</section>
</div>
</div>
</div>
<div id="definition-ScheduleVideoUpdate" class="definition panel" data-traverse-target="definition-ScheduleVideoUpdate">
<h2 class="panel-title">
<a name="/definitions/ScheduleVideoUpdate"></a>ScheduleVideoUpdate:
<!-- <span class="json-property-type"><span class="json-property-type">object</span>
<span class="json-property-range" title="Value limits"></span>
</span> -->
</h2>
<div class="doc-row">
<div class="doc-copy">
<section class="json-schema-properties">
<dl>
<dt data-property-name="updateAt" class="has-description">
<span class="json-property-name">updateAt:</span>
<span class="json-property-type">dateTime</span>
<span class="json-property-range" title="Value limits"></span>
</dt>
<dd>
<p>When to update the video</p>
</dd>
<dt data-property-name="privacy">
<span class="json-property-name">privacy:</span>
<span class="json-property-type">
<span class="">
<a class="json-schema-ref" href="#/definitions/VideoPrivacy">VideoPrivacy</a>
</span>
</span>
<span class="json-property-range" title="Value limits"></span>
</dt>
</dl>
</section>
</div>
<div class="doc-examples">
<section>
<h5>Example</h5>
<!-- <div class="hljs"> --><pre><code class="hljs lang-json">{
<span class="hljs-attr">&quot;updateAt&quot;</span>: <span class="hljs-string">&quot;dateTime&quot;</span>,
<span class="hljs-attr">&quot;privacy&quot;</span>: <span class="hljs-string">&quot;string&quot;</span>
}
</code></pre>
<!-- </div> -->
</section>

View File

@ -711,6 +711,12 @@ paths:
type: string
enum: [Public, Unlisted]
description: 'Video privacy'
- name: scheduleUpdate
in: formData
required: false
description: 'Schedule an update at a specific datetime'
type:
$ref: '#/definitions/ScheduleVideoUpdate'
responses:
'200':
description: successful operation
@ -864,9 +870,15 @@ paths:
- name: privacy
in: formData
required: true
type: string
enum: [Public, Unlisted, Private]
type:
$ref: '#/definitions/VideoPrivacy'
description: 'Video privacy'
- name: scheduleUpdate
in: formData
required: false
description: 'Schedule an update at a specific datetime'
type:
$ref: '#/definitions/ScheduleVideoUpdate'
responses:
'200':
description: successful operation
@ -1709,3 +1721,12 @@ definitions:
type: string
description:
type: string
ScheduleVideoUpdate:
properties:
updateAt:
type: dateTime
description: 'When to update the video'
required: true
privacy:
$ref: '#/definitions/VideoPrivacy'
required: false