Implement video file replacement in client
This commit is contained in:
parent
12dc3a942a
commit
f42fcb4b58
|
@ -345,6 +345,18 @@
|
|||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container formGroupName="videoFile">
|
||||
<ng-container formGroupName="update">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="videoFileUpdateEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Allow users to upload a new version of their video"
|
||||
>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -225,6 +225,11 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
|||
enabled: null
|
||||
}
|
||||
},
|
||||
videoFile: {
|
||||
update: {
|
||||
enabled: null
|
||||
}
|
||||
},
|
||||
autoBlacklist: {
|
||||
videos: {
|
||||
ofUsers: {
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
<!-- Upload progress/cancel/error/success header -->
|
||||
<div *ngIf="isUploadingVideo && !error" class="upload-progress-cancel">
|
||||
<div class="progress" i18n-title title="Total video uploaded">
|
||||
<div
|
||||
class="progress-bar" role="progressbar"
|
||||
[style]="{ width: videoUploadPercents + '%' }" [attr.aria-valuenow]="videoUploadPercents" aria-valuemin="0" [attr.aria-valuemax]="100"
|
||||
>
|
||||
<span *ngIf="videoUploadPercents === 100 && videoUploaded === false" i18n>Processing…</span>
|
||||
<span *ngIf="videoUploadPercents !== 100 || videoUploaded">{{ videoUploadPercents }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
*ngIf="videoUploaded === false"
|
||||
type="button" class="peertube-button grey-button ms-1" i18n-value="Cancel ongoing upload of a video" value="Cancel" (click)="cancel.emit()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div *ngIf="error && enableRetryAfterError" class="upload-progress-retry">
|
||||
<div class="progress">
|
||||
<div class="progress-bar red" role="progressbar" [style]="{ width: '100%' }" [attr.aria-valuenow]="100" aria-valuemin="0" [attr.aria-valuemax]="100">
|
||||
<span>{{ error }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="button" class="peertube-button grey-button ms-1" i18n-value="Retry failed upload of a video" value="Retry" (click)="retry.emit()" />
|
||||
<input type="button" class="peertube-button grey-button ms-1" i18n-value="Cancel ongoing upload of a video" value="Cancel" (click)="cancel.emit()" />
|
||||
</div>
|
||||
|
||||
<div *ngIf="error && !enableRetryAfterError" class="alert alert-danger">
|
||||
<div i18n>Sorry, but something went wrong</div>
|
||||
{{ error }}
|
||||
</div>
|
|
@ -0,0 +1,30 @@
|
|||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
|
||||
.upload-progress-retry,
|
||||
.upload-progress-cancel {
|
||||
display: flex;
|
||||
margin-bottom: 40px;
|
||||
|
||||
.progress {
|
||||
@include progressbar;
|
||||
|
||||
flex-grow: 1;
|
||||
height: 30px;
|
||||
font-size: 14px;
|
||||
background-color: rgba(11, 204, 41, 0.16);
|
||||
|
||||
.progress-bar {
|
||||
background-color: $green;
|
||||
line-height: 30px;
|
||||
text-align: start;
|
||||
font-weight: $font-semibold;
|
||||
|
||||
span {
|
||||
@include margin-left(13px);
|
||||
|
||||
color: pvar(--mainBackgroundColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { Component, EventEmitter, Input, Output } from '@angular/core'
|
||||
|
||||
@Component({
|
||||
selector: 'my-upload-progress',
|
||||
templateUrl: './upload-progress.component.html',
|
||||
styleUrls: [ './upload-progress.component.scss' ]
|
||||
})
|
||||
export class UploadProgressComponent {
|
||||
@Input() isUploadingVideo: boolean
|
||||
@Input() videoUploadPercents: number
|
||||
@Input() error: string
|
||||
@Input() videoUploaded: boolean
|
||||
@Input() enableRetryAfterError: boolean
|
||||
|
||||
@Output() cancel = new EventEmitter()
|
||||
@Output() retry = new EventEmitter()
|
||||
}
|
|
@ -124,7 +124,7 @@
|
|||
<label i18n for="videoPassword">Password</label>
|
||||
<my-input-text formControlName="videoPassword" inputId="videoPassword" [withCopy]="true" [formError]="formErrors['videoPassword']"></my-input-text>
|
||||
</div>
|
||||
|
||||
|
||||
<div *ngIf="schedulePublicationSelected" class="form-group">
|
||||
<label i18n for="schedulePublicationAt">Schedule publication ({{ calendarTimezone }})</label>
|
||||
<p-calendar
|
||||
|
@ -320,6 +320,8 @@
|
|||
<div class="row advanced-settings">
|
||||
<div class="col-md-12 col-xl-8">
|
||||
|
||||
<ng-content></ng-content>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="previewfile">Video thumbnail</label>
|
||||
|
||||
|
|
|
@ -112,6 +112,11 @@ p-calendar {
|
|||
grid-gap: 30px;
|
||||
}
|
||||
|
||||
.button-file {
|
||||
@include peertube-button-file(max-content);
|
||||
@include orange-button;
|
||||
}
|
||||
|
||||
@include on-small-main-col {
|
||||
.form-columns {
|
||||
grid-template-columns: 1fr;
|
||||
|
|
|
@ -68,6 +68,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
|
|||
@Input() videoSource: VideoSource
|
||||
|
||||
@Input() hideWaitTranscoding = false
|
||||
@Input() updateVideoFileEnabled = false
|
||||
|
||||
@Input() type: VideoEditType
|
||||
@Input() liveVideo: LiveVideo
|
||||
|
|
|
@ -5,9 +5,11 @@ import { SharedGlobalIconModule } from '@app/shared/shared-icons'
|
|||
import { SharedMainModule } from '@app/shared/shared-main'
|
||||
import { SharedVideoLiveModule } from '@app/shared/shared-video-live'
|
||||
import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
|
||||
import { UploadProgressComponent } from './upload-progress.component'
|
||||
import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
|
||||
import { VideoCaptionEditModalContentComponent } from './video-caption-edit-modal-content/video-caption-edit-modal-content.component'
|
||||
import { VideoEditComponent } from './video-edit.component'
|
||||
import { VideoUploadService } from './video-upload.service'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@ -22,7 +24,8 @@ import { VideoEditComponent } from './video-edit.component'
|
|||
declarations: [
|
||||
VideoEditComponent,
|
||||
VideoCaptionAddModalComponent,
|
||||
VideoCaptionEditModalContentComponent
|
||||
VideoCaptionEditModalContentComponent,
|
||||
UploadProgressComponent
|
||||
],
|
||||
|
||||
exports: [
|
||||
|
@ -32,11 +35,13 @@ import { VideoEditComponent } from './video-edit.component'
|
|||
SharedFormModule,
|
||||
SharedGlobalIconModule,
|
||||
|
||||
VideoEditComponent
|
||||
VideoEditComponent,
|
||||
UploadProgressComponent
|
||||
],
|
||||
|
||||
providers: [
|
||||
I18nPrimengCalendarService
|
||||
I18nPrimengCalendarService,
|
||||
VideoUploadService
|
||||
]
|
||||
})
|
||||
export class VideoEditModule { }
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
import { UploaderX, UploadState, UploadxOptions } from 'ngx-uploadx'
|
||||
import { HttpErrorResponse, HttpEventType, HttpHeaders } from '@angular/common/http'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { AuthService, Notifier, ServerService } from '@app/core'
|
||||
import { BytesPipe, VideoService } from '@app/shared/shared-main'
|
||||
import { isIOS } from '@root-helpers/web-browser'
|
||||
import { HttpStatusCode } from '@shared/models'
|
||||
import { UploaderXFormData } from './uploaderx-form-data'
|
||||
|
||||
@Injectable()
|
||||
export class VideoUploadService {
|
||||
|
||||
constructor (
|
||||
private server: ServerService,
|
||||
private notifier: Notifier,
|
||||
private authService: AuthService
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
getVideoExtensions () {
|
||||
return this.server.getHTMLConfig().video.file.extensions
|
||||
}
|
||||
|
||||
checkQuotaAndNotify (videoFile: File, maxQuota: number, quotaUsed: number) {
|
||||
const bytePipes = new BytesPipe()
|
||||
|
||||
// Check global user quota
|
||||
if (maxQuota !== -1 && (quotaUsed + videoFile.size) > maxQuota) {
|
||||
const videoSizeBytes = bytePipes.transform(videoFile.size, 0)
|
||||
const videoQuotaUsedBytes = bytePipes.transform(quotaUsed, 0)
|
||||
const videoQuotaBytes = bytePipes.transform(maxQuota, 0)
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
const msg = $localize`Your video quota is exceeded with this video (video size: ${videoSizeBytes}, used: ${videoQuotaUsedBytes}, quota: ${videoQuotaBytes})`
|
||||
this.notifier.error(msg)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
isAudioFile (filename: string) {
|
||||
const extensions = [ '.mp3', '.flac', '.ogg', '.wma', '.wav' ]
|
||||
|
||||
return extensions.some(e => filename.endsWith(e))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getNewUploadxOptions (): UploadxOptions {
|
||||
return this.getUploadxOptions(
|
||||
VideoService.BASE_VIDEO_URL + '/upload-resumable',
|
||||
UploaderXFormData
|
||||
)
|
||||
}
|
||||
|
||||
getReplaceUploadxOptions (videoId: string): UploadxOptions {
|
||||
return this.getUploadxOptions(
|
||||
VideoService.BASE_VIDEO_URL + '/' + videoId + '/source/replace-resumable',
|
||||
UploaderX
|
||||
)
|
||||
}
|
||||
|
||||
private getUploadxOptions (endpoint: string, uploaderClass: typeof UploaderXFormData) {
|
||||
// FIXME: https://github.com/Chocobozzz/PeerTube/issues/4382#issuecomment-915854167
|
||||
const chunkSize = isIOS()
|
||||
? 0
|
||||
: undefined // Auto chunk size
|
||||
|
||||
return {
|
||||
endpoint,
|
||||
multiple: false,
|
||||
|
||||
maxChunkSize: this.server.getHTMLConfig().client.videos.resumableUpload.maxChunkSize,
|
||||
chunkSize,
|
||||
|
||||
token: this.authService.getAccessToken(),
|
||||
|
||||
uploaderClass,
|
||||
|
||||
retryConfig: {
|
||||
maxAttempts: 30, // maximum attempts for 503 codes, otherwise set to 6, see below
|
||||
maxDelay: 120_000, // 2 min
|
||||
shouldRetry: (code: number, attempts: number) => {
|
||||
return code === HttpStatusCode.SERVICE_UNAVAILABLE_503 || ((code < 400 || code > 500) && attempts < 6)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
buildHTTPErrorResponse (state: UploadState): HttpErrorResponse {
|
||||
const error = state.response?.error?.message || state.response?.error || 'Unknown error'
|
||||
|
||||
return {
|
||||
error: new Error(error),
|
||||
name: 'HttpErrorResponse',
|
||||
message: error,
|
||||
ok: false,
|
||||
headers: new HttpHeaders(state.responseHeaders),
|
||||
status: +state.responseStatus,
|
||||
statusText: error,
|
||||
type: HttpEventType.Response,
|
||||
url: state.url
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,13 +2,13 @@
|
|||
<div class="first-step-block">
|
||||
<my-global-icon class="upload-icon" iconName="upload" aria-hidden="true"></my-global-icon>
|
||||
|
||||
<div class="button-file form-control" [ngbTooltip]="'(extensions: ' + videoExtensions + ')'">
|
||||
<div class="button-file form-control" [ngbTooltip]="'(extensions: ' + getVideoExtensions() + ')'">
|
||||
<span i18n>Select the file to upload</span>
|
||||
<input
|
||||
aria-label="Select the file to upload"
|
||||
i18n-aria-label
|
||||
#videofileInput
|
||||
[accept]="videoExtensions"
|
||||
[accept]="getVideoExtensions()"
|
||||
(change)="onFileChange($event)"
|
||||
id="videofile"
|
||||
type="file"
|
||||
|
@ -58,35 +58,11 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload progress/cancel/error/success header -->
|
||||
<div *ngIf="isUploadingVideo && !error" class="upload-progress-cancel">
|
||||
<div class="progress" i18n-title title="Total video uploaded">
|
||||
<div class="progress-bar" role="progressbar" [style]="{ width: videoUploadPercents + '%' }" [attr.aria-valuenow]="videoUploadPercents" aria-valuemin="0" [attr.aria-valuemax]="100">
|
||||
<span *ngIf="videoUploadPercents === 100 && videoUploaded === false" i18n>Processing…</span>
|
||||
<span *ngIf="videoUploadPercents !== 100 || videoUploaded">{{ videoUploadPercents }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
*ngIf="videoUploaded === false"
|
||||
type="button" class="peertube-button grey-button ms-1" i18n-value="Cancel ongoing upload of a video" value="Cancel" (click)="cancelUpload()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div *ngIf="error && enableRetryAfterError" class="upload-progress-retry">
|
||||
<div class="progress">
|
||||
<div class="progress-bar red" role="progressbar" [style]="{ width: '100%' }" [attr.aria-valuenow]="100" aria-valuemin="0" [attr.aria-valuemax]="100">
|
||||
<span>{{ error }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="button" class="peertube-button grey-button ms-1" i18n-value="Retry failed upload of a video" value="Retry" (click)="retryUpload()" />
|
||||
<input type="button" class="peertube-button grey-button ms-1" i18n-value="Cancel ongoing upload of a video" value="Cancel" (click)="cancelUpload()" />
|
||||
</div>
|
||||
|
||||
<div *ngIf="error && !enableRetryAfterError" class="alert alert-danger">
|
||||
<div i18n>Sorry, but something went wrong</div>
|
||||
{{ error }}
|
||||
</div>
|
||||
<my-upload-progress
|
||||
[isUploadingVideo]="isUploadingVideo" [videoUploadPercents]="videoUploadPercents" [error]="error" [videoUploaded]="videoUploaded"
|
||||
[enableRetryAfterError]="enableRetryAfterError" (cancel)="cancelUpload()" (retry)="retryUpload()"
|
||||
>
|
||||
</my-upload-progress>
|
||||
|
||||
<div *ngIf="videoUploaded && !error" class="alert pt-alert-primary" i18n>
|
||||
Congratulations! Your video is now available in your private library.
|
||||
|
|
|
@ -15,31 +15,3 @@
|
|||
margin: 30px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-progress-retry,
|
||||
.upload-progress-cancel {
|
||||
display: flex;
|
||||
margin-bottom: 40px;
|
||||
|
||||
.progress {
|
||||
@include progressbar;
|
||||
|
||||
flex-grow: 1;
|
||||
height: 30px;
|
||||
font-size: 14px;
|
||||
background-color: rgba(11, 204, 41, 0.16);
|
||||
|
||||
.progress-bar {
|
||||
background-color: $green;
|
||||
line-height: 30px;
|
||||
text-align: start;
|
||||
font-weight: $font-semibold;
|
||||
|
||||
span {
|
||||
@include margin-left(13px);
|
||||
|
||||
color: pvar(--mainBackgroundColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
import { truncate } from 'lodash-es'
|
||||
import { UploadState, UploadxOptions, UploadxService } from 'ngx-uploadx'
|
||||
import { HttpErrorResponse, HttpEventType, HttpHeaders } from '@angular/common/http'
|
||||
import { UploadState, UploadxService } from 'ngx-uploadx'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { HttpErrorResponse } from '@angular/common/http'
|
||||
import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { AuthService, CanComponentDeactivate, HooksService, MetaService, Notifier, ServerService, UserService } from '@app/core'
|
||||
import { genericUploadErrorHandler, scrollToTop } from '@app/helpers'
|
||||
import { FormReactiveService } from '@app/shared/shared-forms'
|
||||
import { BytesPipe, Video, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
|
||||
import { Video, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
|
||||
import { LoadingBarService } from '@ngx-loading-bar/core'
|
||||
import { logger } from '@root-helpers/logger'
|
||||
import { isIOS } from '@root-helpers/web-browser'
|
||||
import { HttpStatusCode, VideoCreateResult } from '@shared/models'
|
||||
import { UploaderXFormData } from './uploaderx-form-data'
|
||||
import { VideoUploadService } from '../shared/video-upload.service'
|
||||
import { VideoSend } from './video-send'
|
||||
import { Subscription } from 'rxjs'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-upload',
|
||||
|
@ -49,9 +48,6 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
|||
error: string
|
||||
enableRetryAfterError: boolean
|
||||
|
||||
// So that it can be accessed in the template
|
||||
protected readonly BASE_VIDEO_UPLOAD_URL = VideoService.BASE_VIDEO_URL + '/upload-resumable'
|
||||
|
||||
private isUpdatingVideo = false
|
||||
private fileToUpload: File
|
||||
|
||||
|
@ -72,15 +68,12 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
|||
private hooks: HooksService,
|
||||
private resumableUploadService: UploadxService,
|
||||
private metaService: MetaService,
|
||||
private route: ActivatedRoute
|
||||
private route: ActivatedRoute,
|
||||
private videoUploadService: VideoUploadService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
get videoExtensions () {
|
||||
return this.serverConfig.video.file.extensions.join(', ')
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
super.ngOnInit()
|
||||
|
||||
|
@ -133,28 +126,20 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
|||
}
|
||||
}
|
||||
|
||||
getVideoExtensions () {
|
||||
return this.videoUploadService.getVideoExtensions().join(', ')
|
||||
}
|
||||
|
||||
onUploadVideoOngoing (state: UploadState) {
|
||||
switch (state.status) {
|
||||
case 'error': {
|
||||
if (!this.alreadyRefreshedToken && state.responseStatus === HttpStatusCode.UNAUTHORIZED_401) {
|
||||
this.alreadyRefreshedToken = true
|
||||
|
||||
return this.refereshTokenAndRetryUpload()
|
||||
return this.refreshTokenAndRetryUpload()
|
||||
}
|
||||
|
||||
const error = state.response?.error?.message || state.response?.error || 'Unknown error'
|
||||
|
||||
this.handleUploadError({
|
||||
error: new Error(error),
|
||||
name: 'HttpErrorResponse',
|
||||
message: error,
|
||||
ok: false,
|
||||
headers: new HttpHeaders(state.responseHeaders),
|
||||
status: +state.responseStatus,
|
||||
statusText: error,
|
||||
type: HttpEventType.Response,
|
||||
url: state.url
|
||||
})
|
||||
this.handleUploadError(this.videoUploadService.buildHTTPErrorResponse(state))
|
||||
break
|
||||
}
|
||||
|
||||
|
@ -203,10 +188,12 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
|||
|
||||
if (!file) return
|
||||
|
||||
if (!this.checkGlobalUserQuota(file)) return
|
||||
if (!this.checkDailyUserQuota(file)) return
|
||||
const user = this.authService.getUser()
|
||||
|
||||
if (this.isAudioFile(file.name)) {
|
||||
if (!this.videoUploadService.checkQuotaAndNotify(file, user.videoQuota, this.userVideoQuotaUsed)) return
|
||||
if (!this.videoUploadService.checkQuotaAndNotify(file, user.videoQuotaDaily, this.userVideoQuotaUsedDaily)) return
|
||||
|
||||
if (this.videoUploadService.isAudioFile(file.name)) {
|
||||
this.isUploadingAudioFile = true
|
||||
return
|
||||
}
|
||||
|
@ -291,7 +278,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
|||
}
|
||||
|
||||
this.resumableUploadService.handleFiles(file, {
|
||||
...this.getUploadxOptions(),
|
||||
...this.videoUploadService.getNewUploadxOptions(),
|
||||
|
||||
metadata
|
||||
})
|
||||
|
@ -331,51 +318,6 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
|||
this.updateTitle()
|
||||
}
|
||||
|
||||
private checkGlobalUserQuota (videofile: File) {
|
||||
const bytePipes = new BytesPipe()
|
||||
|
||||
// Check global user quota
|
||||
const videoQuota = this.authService.getUser().videoQuota
|
||||
if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) {
|
||||
const videoSizeBytes = bytePipes.transform(videofile.size, 0)
|
||||
const videoQuotaUsedBytes = bytePipes.transform(this.userVideoQuotaUsed, 0)
|
||||
const videoQuotaBytes = bytePipes.transform(videoQuota, 0)
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
const msg = $localize`Your video quota is exceeded with this video (video size: ${videoSizeBytes}, used: ${videoQuotaUsedBytes}, quota: ${videoQuotaBytes})`
|
||||
this.notifier.error(msg)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private checkDailyUserQuota (videofile: File) {
|
||||
const bytePipes = new BytesPipe()
|
||||
|
||||
// Check daily user quota
|
||||
const videoQuotaDaily = this.authService.getUser().videoQuotaDaily
|
||||
if (videoQuotaDaily !== -1 && (this.userVideoQuotaUsedDaily + videofile.size) > videoQuotaDaily) {
|
||||
const videoSizeBytes = bytePipes.transform(videofile.size, 0)
|
||||
const quotaUsedDailyBytes = bytePipes.transform(this.userVideoQuotaUsedDaily, 0)
|
||||
const quotaDailyBytes = bytePipes.transform(videoQuotaDaily, 0)
|
||||
// eslint-disable-next-line max-len
|
||||
const msg = $localize`Your daily video quota is exceeded with this video (video size: ${videoSizeBytes}, used: ${quotaUsedDailyBytes}, quota: ${quotaDailyBytes})`
|
||||
this.notifier.error(msg)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private isAudioFile (filename: string) {
|
||||
const extensions = [ '.mp3', '.flac', '.ogg', '.wma', '.wav' ]
|
||||
|
||||
return extensions.some(e => filename.endsWith(e))
|
||||
}
|
||||
|
||||
private buildVideoFilename (filename: string) {
|
||||
const nameWithoutExtension = filename.replace(/\.[^/.]+$/, '')
|
||||
let name = nameWithoutExtension.length < 3
|
||||
|
@ -390,35 +332,8 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
|||
return name
|
||||
}
|
||||
|
||||
private refereshTokenAndRetryUpload () {
|
||||
private refreshTokenAndRetryUpload () {
|
||||
this.authService.refreshAccessToken()
|
||||
.subscribe(() => this.retryUpload())
|
||||
}
|
||||
|
||||
private getUploadxOptions (): UploadxOptions {
|
||||
// FIXME: https://github.com/Chocobozzz/PeerTube/issues/4382#issuecomment-915854167
|
||||
const chunkSize = isIOS()
|
||||
? 0
|
||||
: undefined // Auto chunk size
|
||||
|
||||
return {
|
||||
endpoint: this.BASE_VIDEO_UPLOAD_URL,
|
||||
multiple: false,
|
||||
|
||||
maxChunkSize: this.serverConfig.client.videos.resumableUpload.maxChunkSize,
|
||||
chunkSize,
|
||||
|
||||
token: this.authService.getAccessToken(),
|
||||
|
||||
uploaderClass: UploaderXFormData,
|
||||
|
||||
retryConfig: {
|
||||
maxAttempts: 30, // maximum attempts for 503 codes, otherwise set to 6, see below
|
||||
maxDelay: 120_000, // 2 min
|
||||
shouldRetry: (code: number, attempts: number) => {
|
||||
return code === HttpStatusCode.SERVICE_UNAVAILABLE_503 || ((code < 400 || code > 500) && attempts < 6)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,12 @@
|
|||
<a [routerLink]="getVideoUrl()">{{ videoDetails?.name }}</a>
|
||||
</div>
|
||||
|
||||
<my-upload-progress
|
||||
[isUploadingVideo]="isReplacingVideoFile" [videoUploadPercents]="videoUploadPercents" [error]="uploadError" [videoUploaded]="updateDone"
|
||||
[enableRetryAfterError]="false" (cancel)="cancelUpload()"
|
||||
>
|
||||
</my-upload-progress>
|
||||
|
||||
<form novalidate [formGroup]="form">
|
||||
|
||||
<my-video-edit
|
||||
|
@ -12,10 +18,27 @@
|
|||
[videoCaptions]="videoCaptions" [hideWaitTranscoding]="isWaitTranscodingHidden()"
|
||||
type="update" (pluginFieldsAdded)="hydratePluginFieldsFromVideo()"
|
||||
[liveVideo]="liveVideo" [videoToUpdate]="videoDetails"
|
||||
[videoSource]="videoSource"
|
||||
[videoSource]="videoSource" [updateVideoFileEnabled]="isUpdateVideoFileEnabled()"
|
||||
|
||||
(formBuilt)="onFormBuilt()"
|
||||
></my-video-edit>
|
||||
>
|
||||
|
||||
<div *ngIf="isUpdateVideoFileEnabled()" class="form-group">
|
||||
<label class="mb-0" i18n for="videofile">Replace video file</label>
|
||||
|
||||
<div i18n class="form-group-description">⚠️ Uploading a new version of your video will completely erase the current version</div>
|
||||
|
||||
<div>
|
||||
<my-reactive-file
|
||||
formControlName="replaceFile"
|
||||
i18n-inputLabel inputLabel="Select the file to upload"
|
||||
inputName="videofile" [extensions]="getVideoExtensions()" [displayFilename]="true" [displayReset]="true"
|
||||
[buttonTooltip]="'(extensions: ' + getVideoExtensions() + ')'"
|
||||
theme="primary"
|
||||
></my-reactive-file>
|
||||
</div>
|
||||
</div>
|
||||
</my-video-edit>
|
||||
|
||||
<div class="submit-container">
|
||||
<my-button className="orange-button" i18n-label label="Update" icon="circle-tick"
|
||||
|
|
|
@ -1,25 +1,31 @@
|
|||
import { of } from 'rxjs'
|
||||
import { switchMap } from 'rxjs/operators'
|
||||
import debug from 'debug'
|
||||
import { UploadState, UploadxService } from 'ngx-uploadx'
|
||||
import { of, Subject, Subscription } from 'rxjs'
|
||||
import { catchError, map, switchMap } from 'rxjs/operators'
|
||||
import { SelectChannelItem } from 'src/types/select-options-item.model'
|
||||
import { Component, HostListener, OnInit } from '@angular/core'
|
||||
import { HttpErrorResponse } from '@angular/common/http'
|
||||
import { Component, HostListener, OnDestroy, OnInit } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { Notifier } from '@app/core'
|
||||
import { AuthService, CanComponentDeactivate, ConfirmService, Notifier, ServerService, UserService } from '@app/core'
|
||||
import { genericUploadErrorHandler } from '@app/helpers'
|
||||
import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
|
||||
import { Video, VideoCaptionEdit, VideoCaptionService, VideoDetails, VideoEdit, VideoService } from '@app/shared/shared-main'
|
||||
import { LiveVideoService } from '@app/shared/shared-video-live'
|
||||
import { LoadingBarService } from '@ngx-loading-bar/core'
|
||||
import { logger } from '@root-helpers/logger'
|
||||
import { pick, simpleObjectsDeepEqual } from '@shared/core-utils'
|
||||
import { LiveVideo, LiveVideoUpdate, VideoPrivacy, VideoState } from '@shared/models'
|
||||
import { HttpStatusCode, LiveVideo, LiveVideoUpdate, VideoPrivacy, VideoState } from '@shared/models'
|
||||
import { VideoSource } from '@shared/models/videos/video-source'
|
||||
import { hydrateFormFromVideo } from './shared/video-edit-utils'
|
||||
import { VideoUploadService } from './shared/video-upload.service'
|
||||
|
||||
const debugLogger = debug('peertube:video-update')
|
||||
|
||||
@Component({
|
||||
selector: 'my-videos-update',
|
||||
styleUrls: [ './shared/video-edit.component.scss' ],
|
||||
templateUrl: './video-update.component.html'
|
||||
})
|
||||
export class VideoUpdateComponent extends FormReactive implements OnInit {
|
||||
export class VideoUpdateComponent extends FormReactive implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||
videoEdit: VideoEdit
|
||||
videoDetails: VideoDetails
|
||||
videoSource: VideoSource
|
||||
|
@ -27,10 +33,23 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
|
|||
videoCaptions: VideoCaptionEdit[] = []
|
||||
liveVideo: LiveVideo
|
||||
|
||||
userVideoQuotaUsed = 0
|
||||
userVideoQuotaUsedDaily = 0
|
||||
|
||||
isUpdatingVideo = false
|
||||
forbidScheduledPublication = false
|
||||
|
||||
private updateDone = false
|
||||
isReplacingVideoFile = false
|
||||
videoUploadPercents: number
|
||||
uploadError: string
|
||||
|
||||
updateDone = false
|
||||
|
||||
private videoReplacementUploadedSubject = new Subject<void>()
|
||||
private alreadyRefreshedToken = false
|
||||
|
||||
private uploadServiceSubscription: Subscription
|
||||
private updateSubcription: Subscription
|
||||
|
||||
constructor (
|
||||
protected formReactiveService: FormReactiveService,
|
||||
|
@ -40,13 +59,30 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
|
|||
private videoService: VideoService,
|
||||
private loadingBar: LoadingBarService,
|
||||
private videoCaptionService: VideoCaptionService,
|
||||
private liveVideoService: LiveVideoService
|
||||
private server: ServerService,
|
||||
private liveVideoService: LiveVideoService,
|
||||
private videoUploadService: VideoUploadService,
|
||||
private confirmService: ConfirmService,
|
||||
private auth: AuthService,
|
||||
private userService: UserService,
|
||||
private resumableUploadService: UploadxService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.buildForm({})
|
||||
this.buildForm({
|
||||
replaceFile: null
|
||||
})
|
||||
|
||||
this.userService.getMyVideoQuotaUsed()
|
||||
.subscribe(data => {
|
||||
this.userVideoQuotaUsed = data.videoQuotaUsed
|
||||
this.userVideoQuotaUsedDaily = data.videoQuotaUsedDaily
|
||||
})
|
||||
|
||||
this.uploadServiceSubscription = this.resumableUploadService.events
|
||||
.subscribe(state => this.onUploadVideoOngoing(state))
|
||||
|
||||
const { videoData } = this.route.snapshot.data
|
||||
const { video, videoChannels, videoCaptions, videoSource, liveVideo, videoPassword } = videoData
|
||||
|
@ -62,6 +98,12 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
|
|||
this.forbidScheduledPublication = this.videoEdit.privacy !== VideoPrivacy.PRIVATE
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.resumableUploadService.disconnect()
|
||||
|
||||
if (this.uploadServiceSubscription) this.uploadServiceSubscription.unsubscribe()
|
||||
}
|
||||
|
||||
onFormBuilt () {
|
||||
hydrateFormFromVideo(this.form, this.videoEdit, true)
|
||||
|
||||
|
@ -88,6 +130,13 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
|
|||
canDeactivate (): { canDeactivate: boolean, text?: string } {
|
||||
if (this.updateDone === true) return { canDeactivate: true }
|
||||
|
||||
if (this.isUpdatingVideo) {
|
||||
return {
|
||||
canDeactivate: false,
|
||||
text: $localize`Your video is currenctly being updated. If you leave, your changes will be lost.`
|
||||
}
|
||||
}
|
||||
|
||||
const text = $localize`You have unsaved changes! If you leave, your changes will be lost.`
|
||||
|
||||
for (const caption of this.videoCaptions) {
|
||||
|
@ -97,68 +146,90 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
|
|||
return { canDeactivate: this.formChanged === false, text }
|
||||
}
|
||||
|
||||
getVideoExtensions () {
|
||||
return this.videoUploadService.getVideoExtensions()
|
||||
}
|
||||
|
||||
isWaitTranscodingHidden () {
|
||||
return this.videoDetails.state.id !== VideoState.TO_TRANSCODE
|
||||
}
|
||||
|
||||
isUpdateVideoFileEnabled () {
|
||||
if (!this.server.getHTMLConfig().videoFile.update.enabled) return false
|
||||
|
||||
if (this.videoDetails.isLive) return false
|
||||
if (this.videoDetails.state.id !== VideoState.PUBLISHED) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
async update () {
|
||||
await this.waitPendingCheck()
|
||||
this.forceCheck()
|
||||
|
||||
if (!this.form.valid || this.isUpdatingVideo === true) {
|
||||
return
|
||||
}
|
||||
if (!this.form.valid || this.isUpdatingVideo === true) return
|
||||
|
||||
// Check and warn users about a file replacement
|
||||
if (!await this.checkAndConfirmVideoFileReplacement()) return
|
||||
|
||||
this.videoEdit.patch(this.form.value)
|
||||
|
||||
this.abortUpdateIfNeeded()
|
||||
|
||||
this.loadingBar.useRef().start()
|
||||
this.isUpdatingVideo = true
|
||||
|
||||
// Update the video
|
||||
this.videoService.updateVideo(this.videoEdit)
|
||||
.pipe(
|
||||
// Then update captions
|
||||
switchMap(() => this.videoCaptionService.updateCaptions(this.videoEdit.id, this.videoCaptions)),
|
||||
this.updateSubcription = this.videoReplacementUploadedSubject.pipe(
|
||||
switchMap(() => this.videoService.updateVideo(this.videoEdit)),
|
||||
|
||||
switchMap(() => {
|
||||
if (!this.liveVideo) return of(undefined)
|
||||
// Then update captions
|
||||
switchMap(() => this.videoCaptionService.updateCaptions(this.videoEdit.id, this.videoCaptions)),
|
||||
|
||||
const saveReplay = !!this.form.value.saveReplay
|
||||
const replaySettings = saveReplay
|
||||
? { privacy: this.form.value.replayPrivacy }
|
||||
: undefined
|
||||
switchMap(() => {
|
||||
if (!this.liveVideo) return of(undefined)
|
||||
|
||||
const liveVideoUpdate: LiveVideoUpdate = {
|
||||
saveReplay,
|
||||
replaySettings,
|
||||
permanentLive: !!this.form.value.permanentLive,
|
||||
latencyMode: this.form.value.latencyMode
|
||||
}
|
||||
const saveReplay = !!this.form.value.saveReplay
|
||||
const replaySettings = saveReplay
|
||||
? { privacy: this.form.value.replayPrivacy }
|
||||
: undefined
|
||||
|
||||
// Don't update live attributes if they did not change
|
||||
const baseVideo = pick(this.liveVideo, Object.keys(liveVideoUpdate) as (keyof LiveVideoUpdate)[])
|
||||
const liveChanged = !simpleObjectsDeepEqual(baseVideo, liveVideoUpdate)
|
||||
if (!liveChanged) return of(undefined)
|
||||
const liveVideoUpdate: LiveVideoUpdate = {
|
||||
saveReplay,
|
||||
replaySettings,
|
||||
permanentLive: !!this.form.value.permanentLive,
|
||||
latencyMode: this.form.value.latencyMode
|
||||
}
|
||||
|
||||
return this.liveVideoService.updateLive(this.videoEdit.id, liveVideoUpdate)
|
||||
})
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.updateDone = true
|
||||
this.isUpdatingVideo = false
|
||||
this.loadingBar.useRef().complete()
|
||||
this.notifier.success($localize`Video updated.`)
|
||||
this.router.navigateByUrl(Video.buildWatchUrl(this.videoEdit))
|
||||
},
|
||||
// Don't update live attributes if they did not change
|
||||
const baseVideo = pick(this.liveVideo, Object.keys(liveVideoUpdate) as (keyof LiveVideoUpdate)[])
|
||||
const liveChanged = !simpleObjectsDeepEqual(baseVideo, liveVideoUpdate)
|
||||
if (!liveChanged) return of(undefined)
|
||||
|
||||
error: err => {
|
||||
this.loadingBar.useRef().complete()
|
||||
this.isUpdatingVideo = false
|
||||
this.notifier.error(err.message)
|
||||
logger.error(err)
|
||||
}
|
||||
})
|
||||
return this.liveVideoService.updateLive(this.videoEdit.id, liveVideoUpdate)
|
||||
}),
|
||||
|
||||
map(() => true),
|
||||
|
||||
catchError(err => {
|
||||
this.notifier.error(err.message)
|
||||
|
||||
return of(false)
|
||||
})
|
||||
)
|
||||
.subscribe({
|
||||
next: success => {
|
||||
this.isUpdatingVideo = false
|
||||
this.loadingBar.useRef().complete()
|
||||
|
||||
if (!success) return
|
||||
|
||||
this.updateDone = true
|
||||
this.notifier.success($localize`Video updated.`)
|
||||
this.router.navigateByUrl(Video.buildWatchUrl(this.videoEdit))
|
||||
}
|
||||
})
|
||||
|
||||
this.replaceFileIfNeeded()
|
||||
}
|
||||
|
||||
hydratePluginFieldsFromVideo () {
|
||||
|
@ -172,4 +243,118 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
|
|||
getVideoUrl () {
|
||||
return Video.buildWatchUrl(this.videoDetails)
|
||||
}
|
||||
|
||||
private async checkAndConfirmVideoFileReplacement () {
|
||||
const replaceFile: File = this.form.value['replaceFile']
|
||||
if (!replaceFile) return true
|
||||
|
||||
const user = this.auth.getUser()
|
||||
if (!this.videoUploadService.checkQuotaAndNotify(replaceFile, user.videoQuota, this.userVideoQuotaUsed)) return
|
||||
if (!this.videoUploadService.checkQuotaAndNotify(replaceFile, user.videoQuotaDaily, this.userVideoQuotaUsedDaily)) return
|
||||
|
||||
const willBeBlocked = this.server.getHTMLConfig().autoBlacklist.videos.ofUsers.enabled === true && !this.videoDetails.blacklisted
|
||||
let blockedWarning = ''
|
||||
if (willBeBlocked) {
|
||||
// eslint-disable-next-line max-len
|
||||
blockedWarning = ' ' + $localize`Your video will also be automatically blocked since video publication requires manual validation by moderators.`
|
||||
}
|
||||
|
||||
const message = $localize`Uploading a new version of your video will completely erase the current version.` +
|
||||
blockedWarning +
|
||||
' ' +
|
||||
$localize`<br /><br />Do you still want to replace your video file?`
|
||||
|
||||
const res = await this.confirmService.confirm(message, $localize`Replace file warning`)
|
||||
if (res === false) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private replaceFileIfNeeded () {
|
||||
if (!this.form.value['replaceFile']) {
|
||||
this.videoReplacementUploadedSubject.next()
|
||||
return
|
||||
}
|
||||
|
||||
this.uploadFileReplacement(this.form.value['replaceFile'])
|
||||
}
|
||||
|
||||
private uploadFileReplacement (file: File) {
|
||||
const metadata = {
|
||||
filename: file.name
|
||||
}
|
||||
|
||||
this.resumableUploadService.handleFiles(file, {
|
||||
...this.videoUploadService.getReplaceUploadxOptions(this.videoDetails.uuid),
|
||||
|
||||
metadata
|
||||
})
|
||||
|
||||
this.isReplacingVideoFile = true
|
||||
}
|
||||
|
||||
onUploadVideoOngoing (state: UploadState) {
|
||||
debugLogger('Upload state update', state)
|
||||
|
||||
switch (state.status) {
|
||||
case 'error': {
|
||||
if (!this.alreadyRefreshedToken && state.responseStatus === HttpStatusCode.UNAUTHORIZED_401) {
|
||||
this.alreadyRefreshedToken = true
|
||||
|
||||
return this.refreshTokenAndRetryUpload()
|
||||
}
|
||||
|
||||
this.handleUploadError(this.videoUploadService.buildHTTPErrorResponse(state))
|
||||
break
|
||||
}
|
||||
|
||||
case 'cancelled':
|
||||
this.isReplacingVideoFile = false
|
||||
this.videoUploadPercents = 0
|
||||
this.uploadError = ''
|
||||
break
|
||||
|
||||
case 'uploading':
|
||||
this.videoUploadPercents = state.progress || 0
|
||||
break
|
||||
|
||||
case 'complete':
|
||||
this.isReplacingVideoFile = false
|
||||
this.videoReplacementUploadedSubject.next()
|
||||
this.videoUploadPercents = 100
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cancelUpload () {
|
||||
debugLogger('Cancelling upload')
|
||||
|
||||
this.resumableUploadService.control({ action: 'cancel' })
|
||||
|
||||
this.abortUpdateIfNeeded()
|
||||
}
|
||||
|
||||
private handleUploadError (err: HttpErrorResponse) {
|
||||
this.videoUploadPercents = 0
|
||||
this.isReplacingVideoFile = false
|
||||
|
||||
this.uploadError = genericUploadErrorHandler({ err, name: $localize`video` })
|
||||
|
||||
this.videoReplacementUploadedSubject.error(err)
|
||||
}
|
||||
|
||||
private refreshTokenAndRetryUpload () {
|
||||
this.auth.refreshAccessToken()
|
||||
.subscribe(() => this.uploadFileReplacement(this.form.value['replaceFile']))
|
||||
}
|
||||
|
||||
private abortUpdateIfNeeded () {
|
||||
if (this.updateSubcription) {
|
||||
this.updateSubcription.unsubscribe()
|
||||
this.updateSubcription = undefined
|
||||
}
|
||||
|
||||
this.videoReplacementUploadedSubject = new Subject<void>()
|
||||
this.loadingBar.useRef().complete()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,11 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!!video.inputFileUpdatedAt" class="attribute attribute-re-uploaded-on">
|
||||
<span i18n class="attribute-label">Video re-upload</span>
|
||||
<span class="attribute-value">{{ video.inputFileUpdatedAt | date: 'short' }}</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!!video.originallyPublishedAt" class="attribute attribute-originally-published-at">
|
||||
<span i18n class="attribute-label">Originally published</span>
|
||||
<span class="attribute-value">{{ video.originallyPublishedAt | date: 'dd MMMM yyyy' }}</span>
|
||||
|
|
|
@ -5,14 +5,15 @@ import { HttpStatusCode } from '@shared/models'
|
|||
function genericUploadErrorHandler (options: {
|
||||
err: Pick<HttpErrorResponse, 'message' | 'status' | 'headers'>
|
||||
name: string
|
||||
notifier: Notifier
|
||||
notifier?: Notifier
|
||||
sticky?: boolean
|
||||
}) {
|
||||
const { err, name, notifier, sticky = false } = options
|
||||
const title = $localize`Upload failed`
|
||||
const message = buildMessage(name, err)
|
||||
|
||||
notifier.error(message, title, null, sticky)
|
||||
if (notifier) notifier.error(message, title, null, sticky)
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div class="root">
|
||||
<div class="button-file" [ngClass]="{ 'with-icon': !!icon }" [ngbTooltip]="buttonTooltip">
|
||||
<div class="button-file" [ngClass]="classes" [ngbTooltip]="buttonTooltip">
|
||||
<my-global-icon *ngIf="icon" [iconName]="icon"></my-global-icon>
|
||||
|
||||
<span>{{ inputLabel }}</span>
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
.button-file {
|
||||
@include peertube-button-file(auto);
|
||||
@include grey-button;
|
||||
|
||||
&.with-icon {
|
||||
@include button-with-icon;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core'
|
||||
import { Component, EventEmitter, forwardRef, Input, OnChanges, OnInit, Output } from '@angular/core'
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import { Notifier } from '@app/core'
|
||||
import { GlobalIconName } from '@app/shared/shared-icons'
|
||||
|
@ -15,7 +15,8 @@ import { GlobalIconName } from '@app/shared/shared-icons'
|
|||
}
|
||||
]
|
||||
})
|
||||
export class ReactiveFileComponent implements OnInit, ControlValueAccessor {
|
||||
export class ReactiveFileComponent implements OnInit, OnChanges, ControlValueAccessor {
|
||||
@Input() theme: 'primary' | 'secondary' = 'secondary'
|
||||
@Input() inputLabel: string
|
||||
@Input() inputName: string
|
||||
@Input() extensions: string[] = []
|
||||
|
@ -29,6 +30,7 @@ export class ReactiveFileComponent implements OnInit, ControlValueAccessor {
|
|||
|
||||
@Output() fileChanged = new EventEmitter<Blob>()
|
||||
|
||||
classes: { [id: string]: boolean } = {}
|
||||
allowedExtensionsMessage = ''
|
||||
fileInputValue: any
|
||||
|
||||
|
@ -44,6 +46,20 @@ export class ReactiveFileComponent implements OnInit, ControlValueAccessor {
|
|||
|
||||
ngOnInit () {
|
||||
this.allowedExtensionsMessage = this.extensions.join(', ')
|
||||
|
||||
this.buildClasses()
|
||||
}
|
||||
|
||||
ngOnChanges () {
|
||||
this.buildClasses()
|
||||
}
|
||||
|
||||
buildClasses () {
|
||||
this.classes = {
|
||||
'with-icon': !!this.icon,
|
||||
'orange-button': this.theme === 'primary',
|
||||
'grey-button': this.theme === 'secondary'
|
||||
}
|
||||
}
|
||||
|
||||
fileChange (event: any) {
|
||||
|
|
|
@ -27,6 +27,8 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
|
|||
|
||||
trackerUrls: string[]
|
||||
|
||||
inputFileUpdatedAt: Date | string
|
||||
|
||||
files: VideoFile[]
|
||||
streamingPlaylists: VideoStreamingPlaylist[]
|
||||
|
||||
|
@ -41,6 +43,8 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
|
|||
this.commentsEnabled = hash.commentsEnabled
|
||||
this.downloadEnabled = hash.downloadEnabled
|
||||
|
||||
this.inputFileUpdatedAt = hash.inputFileUpdatedAt
|
||||
|
||||
this.trackerUrls = hash.trackerUrls
|
||||
|
||||
this.buildLikeAndDislikePercents()
|
||||
|
|
|
@ -26,6 +26,7 @@ export class Video implements VideoServerModel {
|
|||
updatedAt: Date
|
||||
publishedAt: Date
|
||||
originallyPublishedAt: Date | string
|
||||
|
||||
category: VideoConstant<number>
|
||||
licence: VideoConstant<number>
|
||||
language: VideoConstant<string>
|
||||
|
|
|
@ -48,6 +48,8 @@ class EndCard extends Component {
|
|||
suspendedMessage: HTMLElement
|
||||
nextButton: HTMLElement
|
||||
|
||||
private timeout: any
|
||||
|
||||
private onEndedHandler: () => void
|
||||
private onPlayingHandler: () => void
|
||||
|
||||
|
@ -84,6 +86,8 @@ class EndCard extends Component {
|
|||
if (this.onEndedHandler) this.player().off([ 'auto-stopped', 'ended' ], this.onEndedHandler)
|
||||
if (this.onPlayingHandler) this.player().off('playing', this.onPlayingHandler)
|
||||
|
||||
if (this.timeout) clearTimeout(this.timeout)
|
||||
|
||||
super.dispose()
|
||||
}
|
||||
|
||||
|
@ -114,8 +118,6 @@ class EndCard extends Component {
|
|||
}
|
||||
|
||||
showCard (cb: (canceled: boolean) => void) {
|
||||
let timeout: any
|
||||
|
||||
this.autoplayRing.setAttribute('stroke-dasharray', `${this.dashOffsetStart}`)
|
||||
this.autoplayRing.setAttribute('stroke-dashoffset', `${-this.dashOffsetStart}`)
|
||||
|
||||
|
@ -126,17 +128,20 @@ class EndCard extends Component {
|
|||
}
|
||||
|
||||
this.upNextEvents.one('cancel', () => {
|
||||
clearTimeout(timeout)
|
||||
clearTimeout(this.timeout)
|
||||
this.timeout = undefined
|
||||
cb(true)
|
||||
})
|
||||
|
||||
this.upNextEvents.one('playing', () => {
|
||||
clearTimeout(timeout)
|
||||
clearTimeout(this.timeout)
|
||||
this.timeout = undefined
|
||||
cb(true)
|
||||
})
|
||||
|
||||
this.upNextEvents.one('next', () => {
|
||||
clearTimeout(timeout)
|
||||
clearTimeout(this.timeout)
|
||||
this.timeout = undefined
|
||||
cb(false)
|
||||
})
|
||||
|
||||
|
@ -154,19 +159,20 @@ class EndCard extends Component {
|
|||
this.suspendedMessage.innerText = this.options_.suspendedText
|
||||
goToPercent(0)
|
||||
this.ticks = 0
|
||||
timeout = setTimeout(update.bind(this), 300) // checks once supsended can be a bit longer
|
||||
this.timeout = setTimeout(update.bind(this), 300) // checks once supsended can be a bit longer
|
||||
} else if (this.ticks >= this.totalTicks) {
|
||||
clearTimeout(timeout)
|
||||
clearTimeout(this.timeout)
|
||||
this.timeout = undefined
|
||||
cb(false)
|
||||
} else {
|
||||
this.suspendedMessage.innerText = ''
|
||||
tick()
|
||||
timeout = setTimeout(update.bind(this), this.interval)
|
||||
this.timeout = setTimeout(update.bind(this), this.interval)
|
||||
}
|
||||
}
|
||||
|
||||
this.container.style.display = 'block'
|
||||
timeout = setTimeout(update.bind(this), this.interval)
|
||||
this.timeout = setTimeout(update.bind(this), this.interval)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ import { openapiOperationDoc } from '@server/middlewares/doc'
|
|||
import { VideoModel } from '@server/models/video/video'
|
||||
import { VideoSourceModel } from '@server/models/video/video-source'
|
||||
import { MStreamingPlaylistFiles, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
|
||||
import { HttpStatusCode, VideoState } from '@shared/models'
|
||||
import { VideoState } from '@shared/models'
|
||||
import { logger, loggerTagsFactory } from '../../../helpers/logger'
|
||||
import {
|
||||
asyncMiddleware,
|
||||
|
@ -121,7 +121,7 @@ async function replaceVideoSourceResumable (req: express.Request, res: express.R
|
|||
|
||||
await removeOldFiles({ video, files: oldWebVideoFiles, playlists: oldStreamingPlaylists })
|
||||
|
||||
await VideoSourceModel.create({
|
||||
const source = await VideoSourceModel.create({
|
||||
filename: originalFilename,
|
||||
videoId: video.id,
|
||||
createdAt: inputFileUpdatedAt
|
||||
|
@ -135,7 +135,7 @@ async function replaceVideoSourceResumable (req: express.Request, res: express.R
|
|||
|
||||
Hooks.runAction('action:api.video.file-updated', { video, req, res })
|
||||
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
return res.json(source.toFormattedJSON())
|
||||
} finally {
|
||||
videoFileMutexReleaser()
|
||||
}
|
||||
|
|
|
@ -462,7 +462,7 @@ export class VideosCommand extends AbstractCommand {
|
|||
path: string
|
||||
attributes: { fixture?: string } & { [id: string]: any }
|
||||
}): Promise<VideoCreateResult> {
|
||||
const { path, attributes, expectedStatus } = options
|
||||
const { path, attributes, expectedStatus = HttpStatusCode.OK_200 } = options
|
||||
|
||||
let size = 0
|
||||
let videoFilePath: string
|
||||
|
@ -597,43 +597,47 @@ export class VideosCommand extends AbstractCommand {
|
|||
const readable = createReadStream(videoFilePath, { highWaterMark: 8 * 1024 })
|
||||
return new Promise<GotResponse<{ video: VideoCreateResult }>>((resolve, reject) => {
|
||||
readable.on('data', async function onData (chunk) {
|
||||
readable.pause()
|
||||
try {
|
||||
readable.pause()
|
||||
|
||||
const headers = {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Range': contentRangeBuilder
|
||||
? contentRangeBuilder(start, chunk)
|
||||
: `bytes ${start}-${start + chunk.length - 1}/${size}`,
|
||||
'Content-Length': contentLength ? contentLength + '' : chunk.length + ''
|
||||
const headers = {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Range': contentRangeBuilder
|
||||
? contentRangeBuilder(start, chunk)
|
||||
: `bytes ${start}-${start + chunk.length - 1}/${size}`,
|
||||
'Content-Length': contentLength ? contentLength + '' : chunk.length + ''
|
||||
}
|
||||
|
||||
if (digestBuilder) {
|
||||
Object.assign(headers, { digest: digestBuilder(chunk) })
|
||||
}
|
||||
|
||||
const res = await got<{ video: VideoCreateResult }>({
|
||||
url,
|
||||
method: 'put',
|
||||
headers,
|
||||
path: path + '?' + pathUploadId,
|
||||
body: chunk,
|
||||
responseType: 'json',
|
||||
throwHttpErrors: false
|
||||
})
|
||||
|
||||
start += chunk.length
|
||||
|
||||
if (res.statusCode === expectedStatus) {
|
||||
return resolve(res)
|
||||
}
|
||||
|
||||
if (res.statusCode !== HttpStatusCode.PERMANENT_REDIRECT_308) {
|
||||
readable.off('data', onData)
|
||||
return reject(new Error('Incorrect transient behaviour sending intermediary chunks'))
|
||||
}
|
||||
|
||||
readable.resume()
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
|
||||
if (digestBuilder) {
|
||||
Object.assign(headers, { digest: digestBuilder(chunk) })
|
||||
}
|
||||
|
||||
const res = await got<{ video: VideoCreateResult }>({
|
||||
url,
|
||||
method: 'put',
|
||||
headers,
|
||||
path: path + '?' + pathUploadId,
|
||||
body: chunk,
|
||||
responseType: 'json',
|
||||
throwHttpErrors: false
|
||||
})
|
||||
|
||||
start += chunk.length
|
||||
|
||||
if (res.statusCode === expectedStatus) {
|
||||
return resolve(res)
|
||||
}
|
||||
|
||||
if (res.statusCode !== HttpStatusCode.PERMANENT_REDIRECT_308) {
|
||||
readable.off('data', onData)
|
||||
return reject(new Error('Incorrect transient behaviour sending intermediary chunks'))
|
||||
}
|
||||
|
||||
readable.resume()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -695,8 +699,7 @@ export class VideosCommand extends AbstractCommand {
|
|||
...options,
|
||||
|
||||
path: '/api/v1/videos/' + options.videoId + '/source/replace-resumable',
|
||||
attributes: { fixture: options.fixture },
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
attributes: { fixture: options.fixture }
|
||||
})
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue