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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -225,6 +225,11 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
||||||
enabled: null
|
enabled: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
videoFile: {
|
||||||
|
update: {
|
||||||
|
enabled: null
|
||||||
|
}
|
||||||
|
},
|
||||||
autoBlacklist: {
|
autoBlacklist: {
|
||||||
videos: {
|
videos: {
|
||||||
ofUsers: {
|
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>
|
<label i18n for="videoPassword">Password</label>
|
||||||
<my-input-text formControlName="videoPassword" inputId="videoPassword" [withCopy]="true" [formError]="formErrors['videoPassword']"></my-input-text>
|
<my-input-text formControlName="videoPassword" inputId="videoPassword" [withCopy]="true" [formError]="formErrors['videoPassword']"></my-input-text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="schedulePublicationSelected" class="form-group">
|
<div *ngIf="schedulePublicationSelected" class="form-group">
|
||||||
<label i18n for="schedulePublicationAt">Schedule publication ({{ calendarTimezone }})</label>
|
<label i18n for="schedulePublicationAt">Schedule publication ({{ calendarTimezone }})</label>
|
||||||
<p-calendar
|
<p-calendar
|
||||||
|
@ -320,6 +320,8 @@
|
||||||
<div class="row advanced-settings">
|
<div class="row advanced-settings">
|
||||||
<div class="col-md-12 col-xl-8">
|
<div class="col-md-12 col-xl-8">
|
||||||
|
|
||||||
|
<ng-content></ng-content>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label i18n for="previewfile">Video thumbnail</label>
|
<label i18n for="previewfile">Video thumbnail</label>
|
||||||
|
|
||||||
|
|
|
@ -112,6 +112,11 @@ p-calendar {
|
||||||
grid-gap: 30px;
|
grid-gap: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-file {
|
||||||
|
@include peertube-button-file(max-content);
|
||||||
|
@include orange-button;
|
||||||
|
}
|
||||||
|
|
||||||
@include on-small-main-col {
|
@include on-small-main-col {
|
||||||
.form-columns {
|
.form-columns {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|
|
@ -68,6 +68,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
|
||||||
@Input() videoSource: VideoSource
|
@Input() videoSource: VideoSource
|
||||||
|
|
||||||
@Input() hideWaitTranscoding = false
|
@Input() hideWaitTranscoding = false
|
||||||
|
@Input() updateVideoFileEnabled = false
|
||||||
|
|
||||||
@Input() type: VideoEditType
|
@Input() type: VideoEditType
|
||||||
@Input() liveVideo: LiveVideo
|
@Input() liveVideo: LiveVideo
|
||||||
|
|
|
@ -5,9 +5,11 @@ import { SharedGlobalIconModule } from '@app/shared/shared-icons'
|
||||||
import { SharedMainModule } from '@app/shared/shared-main'
|
import { SharedMainModule } from '@app/shared/shared-main'
|
||||||
import { SharedVideoLiveModule } from '@app/shared/shared-video-live'
|
import { SharedVideoLiveModule } from '@app/shared/shared-video-live'
|
||||||
import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
|
import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
|
||||||
|
import { UploadProgressComponent } from './upload-progress.component'
|
||||||
import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
|
import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
|
||||||
import { VideoCaptionEditModalContentComponent } from './video-caption-edit-modal-content/video-caption-edit-modal-content.component'
|
import { VideoCaptionEditModalContentComponent } from './video-caption-edit-modal-content/video-caption-edit-modal-content.component'
|
||||||
import { VideoEditComponent } from './video-edit.component'
|
import { VideoEditComponent } from './video-edit.component'
|
||||||
|
import { VideoUploadService } from './video-upload.service'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -22,7 +24,8 @@ import { VideoEditComponent } from './video-edit.component'
|
||||||
declarations: [
|
declarations: [
|
||||||
VideoEditComponent,
|
VideoEditComponent,
|
||||||
VideoCaptionAddModalComponent,
|
VideoCaptionAddModalComponent,
|
||||||
VideoCaptionEditModalContentComponent
|
VideoCaptionEditModalContentComponent,
|
||||||
|
UploadProgressComponent
|
||||||
],
|
],
|
||||||
|
|
||||||
exports: [
|
exports: [
|
||||||
|
@ -32,11 +35,13 @@ import { VideoEditComponent } from './video-edit.component'
|
||||||
SharedFormModule,
|
SharedFormModule,
|
||||||
SharedGlobalIconModule,
|
SharedGlobalIconModule,
|
||||||
|
|
||||||
VideoEditComponent
|
VideoEditComponent,
|
||||||
|
UploadProgressComponent
|
||||||
],
|
],
|
||||||
|
|
||||||
providers: [
|
providers: [
|
||||||
I18nPrimengCalendarService
|
I18nPrimengCalendarService,
|
||||||
|
VideoUploadService
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class VideoEditModule { }
|
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">
|
<div class="first-step-block">
|
||||||
<my-global-icon class="upload-icon" iconName="upload" aria-hidden="true"></my-global-icon>
|
<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>
|
<span i18n>Select the file to upload</span>
|
||||||
<input
|
<input
|
||||||
aria-label="Select the file to upload"
|
aria-label="Select the file to upload"
|
||||||
i18n-aria-label
|
i18n-aria-label
|
||||||
#videofileInput
|
#videofileInput
|
||||||
[accept]="videoExtensions"
|
[accept]="getVideoExtensions()"
|
||||||
(change)="onFileChange($event)"
|
(change)="onFileChange($event)"
|
||||||
id="videofile"
|
id="videofile"
|
||||||
type="file"
|
type="file"
|
||||||
|
@ -58,35 +58,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Upload progress/cancel/error/success header -->
|
<my-upload-progress
|
||||||
<div *ngIf="isUploadingVideo && !error" class="upload-progress-cancel">
|
[isUploadingVideo]="isUploadingVideo" [videoUploadPercents]="videoUploadPercents" [error]="error" [videoUploaded]="videoUploaded"
|
||||||
<div class="progress" i18n-title title="Total video uploaded">
|
[enableRetryAfterError]="enableRetryAfterError" (cancel)="cancelUpload()" (retry)="retryUpload()"
|
||||||
<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>
|
</my-upload-progress>
|
||||||
<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>
|
|
||||||
|
|
||||||
<div *ngIf="videoUploaded && !error" class="alert pt-alert-primary" i18n>
|
<div *ngIf="videoUploaded && !error" class="alert pt-alert-primary" i18n>
|
||||||
Congratulations! Your video is now available in your private library.
|
Congratulations! Your video is now available in your private library.
|
||||||
|
|
|
@ -15,31 +15,3 @@
|
||||||
margin: 30px 0;
|
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 { truncate } from 'lodash-es'
|
||||||
import { UploadState, UploadxOptions, UploadxService } from 'ngx-uploadx'
|
import { UploadState, UploadxService } from 'ngx-uploadx'
|
||||||
import { HttpErrorResponse, HttpEventType, HttpHeaders } from '@angular/common/http'
|
import { Subscription } from 'rxjs'
|
||||||
|
import { HttpErrorResponse } from '@angular/common/http'
|
||||||
import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
|
import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { AuthService, CanComponentDeactivate, HooksService, MetaService, Notifier, ServerService, UserService } from '@app/core'
|
import { AuthService, CanComponentDeactivate, HooksService, MetaService, Notifier, ServerService, UserService } from '@app/core'
|
||||||
import { genericUploadErrorHandler, scrollToTop } from '@app/helpers'
|
import { genericUploadErrorHandler, scrollToTop } from '@app/helpers'
|
||||||
import { FormReactiveService } from '@app/shared/shared-forms'
|
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 { LoadingBarService } from '@ngx-loading-bar/core'
|
||||||
import { logger } from '@root-helpers/logger'
|
import { logger } from '@root-helpers/logger'
|
||||||
import { isIOS } from '@root-helpers/web-browser'
|
|
||||||
import { HttpStatusCode, VideoCreateResult } from '@shared/models'
|
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 { VideoSend } from './video-send'
|
||||||
import { Subscription } from 'rxjs'
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-upload',
|
selector: 'my-video-upload',
|
||||||
|
@ -49,9 +48,6 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
||||||
error: string
|
error: string
|
||||||
enableRetryAfterError: boolean
|
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 isUpdatingVideo = false
|
||||||
private fileToUpload: File
|
private fileToUpload: File
|
||||||
|
|
||||||
|
@ -72,15 +68,12 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
||||||
private hooks: HooksService,
|
private hooks: HooksService,
|
||||||
private resumableUploadService: UploadxService,
|
private resumableUploadService: UploadxService,
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
private route: ActivatedRoute
|
private route: ActivatedRoute,
|
||||||
|
private videoUploadService: VideoUploadService
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
|
||||||
get videoExtensions () {
|
|
||||||
return this.serverConfig.video.file.extensions.join(', ')
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
super.ngOnInit()
|
super.ngOnInit()
|
||||||
|
|
||||||
|
@ -133,28 +126,20 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getVideoExtensions () {
|
||||||
|
return this.videoUploadService.getVideoExtensions().join(', ')
|
||||||
|
}
|
||||||
|
|
||||||
onUploadVideoOngoing (state: UploadState) {
|
onUploadVideoOngoing (state: UploadState) {
|
||||||
switch (state.status) {
|
switch (state.status) {
|
||||||
case 'error': {
|
case 'error': {
|
||||||
if (!this.alreadyRefreshedToken && state.responseStatus === HttpStatusCode.UNAUTHORIZED_401) {
|
if (!this.alreadyRefreshedToken && state.responseStatus === HttpStatusCode.UNAUTHORIZED_401) {
|
||||||
this.alreadyRefreshedToken = true
|
this.alreadyRefreshedToken = true
|
||||||
|
|
||||||
return this.refereshTokenAndRetryUpload()
|
return this.refreshTokenAndRetryUpload()
|
||||||
}
|
}
|
||||||
|
|
||||||
const error = state.response?.error?.message || state.response?.error || 'Unknown error'
|
this.handleUploadError(this.videoUploadService.buildHTTPErrorResponse(state))
|
||||||
|
|
||||||
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
|
|
||||||
})
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,10 +188,12 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
||||||
|
|
||||||
if (!file) return
|
if (!file) return
|
||||||
|
|
||||||
if (!this.checkGlobalUserQuota(file)) return
|
const user = this.authService.getUser()
|
||||||
if (!this.checkDailyUserQuota(file)) return
|
|
||||||
|
|
||||||
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
|
this.isUploadingAudioFile = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -291,7 +278,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
||||||
}
|
}
|
||||||
|
|
||||||
this.resumableUploadService.handleFiles(file, {
|
this.resumableUploadService.handleFiles(file, {
|
||||||
...this.getUploadxOptions(),
|
...this.videoUploadService.getNewUploadxOptions(),
|
||||||
|
|
||||||
metadata
|
metadata
|
||||||
})
|
})
|
||||||
|
@ -331,51 +318,6 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
||||||
this.updateTitle()
|
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) {
|
private buildVideoFilename (filename: string) {
|
||||||
const nameWithoutExtension = filename.replace(/\.[^/.]+$/, '')
|
const nameWithoutExtension = filename.replace(/\.[^/.]+$/, '')
|
||||||
let name = nameWithoutExtension.length < 3
|
let name = nameWithoutExtension.length < 3
|
||||||
|
@ -390,35 +332,8 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
private refereshTokenAndRetryUpload () {
|
private refreshTokenAndRetryUpload () {
|
||||||
this.authService.refreshAccessToken()
|
this.authService.refreshAccessToken()
|
||||||
.subscribe(() => this.retryUpload())
|
.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>
|
<a [routerLink]="getVideoUrl()">{{ videoDetails?.name }}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<my-upload-progress
|
||||||
|
[isUploadingVideo]="isReplacingVideoFile" [videoUploadPercents]="videoUploadPercents" [error]="uploadError" [videoUploaded]="updateDone"
|
||||||
|
[enableRetryAfterError]="false" (cancel)="cancelUpload()"
|
||||||
|
>
|
||||||
|
</my-upload-progress>
|
||||||
|
|
||||||
<form novalidate [formGroup]="form">
|
<form novalidate [formGroup]="form">
|
||||||
|
|
||||||
<my-video-edit
|
<my-video-edit
|
||||||
|
@ -12,10 +18,27 @@
|
||||||
[videoCaptions]="videoCaptions" [hideWaitTranscoding]="isWaitTranscodingHidden()"
|
[videoCaptions]="videoCaptions" [hideWaitTranscoding]="isWaitTranscodingHidden()"
|
||||||
type="update" (pluginFieldsAdded)="hydratePluginFieldsFromVideo()"
|
type="update" (pluginFieldsAdded)="hydratePluginFieldsFromVideo()"
|
||||||
[liveVideo]="liveVideo" [videoToUpdate]="videoDetails"
|
[liveVideo]="liveVideo" [videoToUpdate]="videoDetails"
|
||||||
[videoSource]="videoSource"
|
[videoSource]="videoSource" [updateVideoFileEnabled]="isUpdateVideoFileEnabled()"
|
||||||
|
|
||||||
(formBuilt)="onFormBuilt()"
|
(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">
|
<div class="submit-container">
|
||||||
<my-button className="orange-button" i18n-label label="Update" icon="circle-tick"
|
<my-button className="orange-button" i18n-label label="Update" icon="circle-tick"
|
||||||
|
|
|
@ -1,25 +1,31 @@
|
||||||
import { of } from 'rxjs'
|
import debug from 'debug'
|
||||||
import { switchMap } from 'rxjs/operators'
|
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 { 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 { 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 { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
|
||||||
import { Video, VideoCaptionEdit, VideoCaptionService, VideoDetails, VideoEdit, VideoService } from '@app/shared/shared-main'
|
import { Video, VideoCaptionEdit, VideoCaptionService, VideoDetails, VideoEdit, VideoService } from '@app/shared/shared-main'
|
||||||
import { LiveVideoService } from '@app/shared/shared-video-live'
|
import { LiveVideoService } from '@app/shared/shared-video-live'
|
||||||
import { LoadingBarService } from '@ngx-loading-bar/core'
|
import { LoadingBarService } from '@ngx-loading-bar/core'
|
||||||
import { logger } from '@root-helpers/logger'
|
|
||||||
import { pick, simpleObjectsDeepEqual } from '@shared/core-utils'
|
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 { VideoSource } from '@shared/models/videos/video-source'
|
||||||
import { hydrateFormFromVideo } from './shared/video-edit-utils'
|
import { hydrateFormFromVideo } from './shared/video-edit-utils'
|
||||||
|
import { VideoUploadService } from './shared/video-upload.service'
|
||||||
|
|
||||||
|
const debugLogger = debug('peertube:video-update')
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-videos-update',
|
selector: 'my-videos-update',
|
||||||
styleUrls: [ './shared/video-edit.component.scss' ],
|
styleUrls: [ './shared/video-edit.component.scss' ],
|
||||||
templateUrl: './video-update.component.html'
|
templateUrl: './video-update.component.html'
|
||||||
})
|
})
|
||||||
export class VideoUpdateComponent extends FormReactive implements OnInit {
|
export class VideoUpdateComponent extends FormReactive implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||||
videoEdit: VideoEdit
|
videoEdit: VideoEdit
|
||||||
videoDetails: VideoDetails
|
videoDetails: VideoDetails
|
||||||
videoSource: VideoSource
|
videoSource: VideoSource
|
||||||
|
@ -27,10 +33,23 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
|
||||||
videoCaptions: VideoCaptionEdit[] = []
|
videoCaptions: VideoCaptionEdit[] = []
|
||||||
liveVideo: LiveVideo
|
liveVideo: LiveVideo
|
||||||
|
|
||||||
|
userVideoQuotaUsed = 0
|
||||||
|
userVideoQuotaUsedDaily = 0
|
||||||
|
|
||||||
isUpdatingVideo = false
|
isUpdatingVideo = false
|
||||||
forbidScheduledPublication = 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 (
|
constructor (
|
||||||
protected formReactiveService: FormReactiveService,
|
protected formReactiveService: FormReactiveService,
|
||||||
|
@ -40,13 +59,30 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
|
||||||
private videoService: VideoService,
|
private videoService: VideoService,
|
||||||
private loadingBar: LoadingBarService,
|
private loadingBar: LoadingBarService,
|
||||||
private videoCaptionService: VideoCaptionService,
|
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()
|
super()
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit () {
|
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 { videoData } = this.route.snapshot.data
|
||||||
const { video, videoChannels, videoCaptions, videoSource, liveVideo, videoPassword } = videoData
|
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
|
this.forbidScheduledPublication = this.videoEdit.privacy !== VideoPrivacy.PRIVATE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy () {
|
||||||
|
this.resumableUploadService.disconnect()
|
||||||
|
|
||||||
|
if (this.uploadServiceSubscription) this.uploadServiceSubscription.unsubscribe()
|
||||||
|
}
|
||||||
|
|
||||||
onFormBuilt () {
|
onFormBuilt () {
|
||||||
hydrateFormFromVideo(this.form, this.videoEdit, true)
|
hydrateFormFromVideo(this.form, this.videoEdit, true)
|
||||||
|
|
||||||
|
@ -88,6 +130,13 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
|
||||||
canDeactivate (): { canDeactivate: boolean, text?: string } {
|
canDeactivate (): { canDeactivate: boolean, text?: string } {
|
||||||
if (this.updateDone === true) return { canDeactivate: true }
|
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.`
|
const text = $localize`You have unsaved changes! If you leave, your changes will be lost.`
|
||||||
|
|
||||||
for (const caption of this.videoCaptions) {
|
for (const caption of this.videoCaptions) {
|
||||||
|
@ -97,68 +146,90 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
|
||||||
return { canDeactivate: this.formChanged === false, text }
|
return { canDeactivate: this.formChanged === false, text }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getVideoExtensions () {
|
||||||
|
return this.videoUploadService.getVideoExtensions()
|
||||||
|
}
|
||||||
|
|
||||||
isWaitTranscodingHidden () {
|
isWaitTranscodingHidden () {
|
||||||
return this.videoDetails.state.id !== VideoState.TO_TRANSCODE
|
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 () {
|
async update () {
|
||||||
await this.waitPendingCheck()
|
await this.waitPendingCheck()
|
||||||
this.forceCheck()
|
this.forceCheck()
|
||||||
|
|
||||||
if (!this.form.valid || this.isUpdatingVideo === true) {
|
if (!this.form.valid || this.isUpdatingVideo === true) return
|
||||||
return
|
|
||||||
}
|
// Check and warn users about a file replacement
|
||||||
|
if (!await this.checkAndConfirmVideoFileReplacement()) return
|
||||||
|
|
||||||
this.videoEdit.patch(this.form.value)
|
this.videoEdit.patch(this.form.value)
|
||||||
|
|
||||||
|
this.abortUpdateIfNeeded()
|
||||||
|
|
||||||
this.loadingBar.useRef().start()
|
this.loadingBar.useRef().start()
|
||||||
this.isUpdatingVideo = true
|
this.isUpdatingVideo = true
|
||||||
|
|
||||||
// Update the video
|
this.updateSubcription = this.videoReplacementUploadedSubject.pipe(
|
||||||
this.videoService.updateVideo(this.videoEdit)
|
switchMap(() => this.videoService.updateVideo(this.videoEdit)),
|
||||||
.pipe(
|
|
||||||
// Then update captions
|
|
||||||
switchMap(() => this.videoCaptionService.updateCaptions(this.videoEdit.id, this.videoCaptions)),
|
|
||||||
|
|
||||||
switchMap(() => {
|
// Then update captions
|
||||||
if (!this.liveVideo) return of(undefined)
|
switchMap(() => this.videoCaptionService.updateCaptions(this.videoEdit.id, this.videoCaptions)),
|
||||||
|
|
||||||
const saveReplay = !!this.form.value.saveReplay
|
switchMap(() => {
|
||||||
const replaySettings = saveReplay
|
if (!this.liveVideo) return of(undefined)
|
||||||
? { privacy: this.form.value.replayPrivacy }
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
const liveVideoUpdate: LiveVideoUpdate = {
|
const saveReplay = !!this.form.value.saveReplay
|
||||||
saveReplay,
|
const replaySettings = saveReplay
|
||||||
replaySettings,
|
? { privacy: this.form.value.replayPrivacy }
|
||||||
permanentLive: !!this.form.value.permanentLive,
|
: undefined
|
||||||
latencyMode: this.form.value.latencyMode
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't update live attributes if they did not change
|
const liveVideoUpdate: LiveVideoUpdate = {
|
||||||
const baseVideo = pick(this.liveVideo, Object.keys(liveVideoUpdate) as (keyof LiveVideoUpdate)[])
|
saveReplay,
|
||||||
const liveChanged = !simpleObjectsDeepEqual(baseVideo, liveVideoUpdate)
|
replaySettings,
|
||||||
if (!liveChanged) return of(undefined)
|
permanentLive: !!this.form.value.permanentLive,
|
||||||
|
latencyMode: this.form.value.latencyMode
|
||||||
|
}
|
||||||
|
|
||||||
return this.liveVideoService.updateLive(this.videoEdit.id, liveVideoUpdate)
|
// 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)
|
||||||
.subscribe({
|
if (!liveChanged) return of(undefined)
|
||||||
next: () => {
|
|
||||||
this.updateDone = true
|
|
||||||
this.isUpdatingVideo = false
|
|
||||||
this.loadingBar.useRef().complete()
|
|
||||||
this.notifier.success($localize`Video updated.`)
|
|
||||||
this.router.navigateByUrl(Video.buildWatchUrl(this.videoEdit))
|
|
||||||
},
|
|
||||||
|
|
||||||
error: err => {
|
return this.liveVideoService.updateLive(this.videoEdit.id, liveVideoUpdate)
|
||||||
this.loadingBar.useRef().complete()
|
}),
|
||||||
this.isUpdatingVideo = false
|
|
||||||
this.notifier.error(err.message)
|
map(() => true),
|
||||||
logger.error(err)
|
|
||||||
}
|
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 () {
|
hydratePluginFieldsFromVideo () {
|
||||||
|
@ -172,4 +243,118 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
|
||||||
getVideoUrl () {
|
getVideoUrl () {
|
||||||
return Video.buildWatchUrl(this.videoDetails)
|
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>
|
</a>
|
||||||
</div>
|
</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">
|
<div *ngIf="!!video.originallyPublishedAt" class="attribute attribute-originally-published-at">
|
||||||
<span i18n class="attribute-label">Originally published</span>
|
<span i18n class="attribute-label">Originally published</span>
|
||||||
<span class="attribute-value">{{ video.originallyPublishedAt | date: 'dd MMMM yyyy' }}</span>
|
<span class="attribute-value">{{ video.originallyPublishedAt | date: 'dd MMMM yyyy' }}</span>
|
||||||
|
|
|
@ -5,14 +5,15 @@ import { HttpStatusCode } from '@shared/models'
|
||||||
function genericUploadErrorHandler (options: {
|
function genericUploadErrorHandler (options: {
|
||||||
err: Pick<HttpErrorResponse, 'message' | 'status' | 'headers'>
|
err: Pick<HttpErrorResponse, 'message' | 'status' | 'headers'>
|
||||||
name: string
|
name: string
|
||||||
notifier: Notifier
|
notifier?: Notifier
|
||||||
sticky?: boolean
|
sticky?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { err, name, notifier, sticky = false } = options
|
const { err, name, notifier, sticky = false } = options
|
||||||
const title = $localize`Upload failed`
|
const title = $localize`Upload failed`
|
||||||
const message = buildMessage(name, err)
|
const message = buildMessage(name, err)
|
||||||
|
|
||||||
notifier.error(message, title, null, sticky)
|
if (notifier) notifier.error(message, title, null, sticky)
|
||||||
|
|
||||||
return message
|
return message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<div class="root">
|
<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>
|
<my-global-icon *ngIf="icon" [iconName]="icon"></my-global-icon>
|
||||||
|
|
||||||
<span>{{ inputLabel }}</span>
|
<span>{{ inputLabel }}</span>
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
.button-file {
|
.button-file {
|
||||||
@include peertube-button-file(auto);
|
@include peertube-button-file(auto);
|
||||||
@include grey-button;
|
|
||||||
|
|
||||||
&.with-icon {
|
&.with-icon {
|
||||||
@include 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 { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||||
import { Notifier } from '@app/core'
|
import { Notifier } from '@app/core'
|
||||||
import { GlobalIconName } from '@app/shared/shared-icons'
|
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() inputLabel: string
|
||||||
@Input() inputName: string
|
@Input() inputName: string
|
||||||
@Input() extensions: string[] = []
|
@Input() extensions: string[] = []
|
||||||
|
@ -29,6 +30,7 @@ export class ReactiveFileComponent implements OnInit, ControlValueAccessor {
|
||||||
|
|
||||||
@Output() fileChanged = new EventEmitter<Blob>()
|
@Output() fileChanged = new EventEmitter<Blob>()
|
||||||
|
|
||||||
|
classes: { [id: string]: boolean } = {}
|
||||||
allowedExtensionsMessage = ''
|
allowedExtensionsMessage = ''
|
||||||
fileInputValue: any
|
fileInputValue: any
|
||||||
|
|
||||||
|
@ -44,6 +46,20 @@ export class ReactiveFileComponent implements OnInit, ControlValueAccessor {
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
this.allowedExtensionsMessage = this.extensions.join(', ')
|
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) {
|
fileChange (event: any) {
|
||||||
|
|
|
@ -27,6 +27,8 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
|
||||||
|
|
||||||
trackerUrls: string[]
|
trackerUrls: string[]
|
||||||
|
|
||||||
|
inputFileUpdatedAt: Date | string
|
||||||
|
|
||||||
files: VideoFile[]
|
files: VideoFile[]
|
||||||
streamingPlaylists: VideoStreamingPlaylist[]
|
streamingPlaylists: VideoStreamingPlaylist[]
|
||||||
|
|
||||||
|
@ -41,6 +43,8 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
|
||||||
this.commentsEnabled = hash.commentsEnabled
|
this.commentsEnabled = hash.commentsEnabled
|
||||||
this.downloadEnabled = hash.downloadEnabled
|
this.downloadEnabled = hash.downloadEnabled
|
||||||
|
|
||||||
|
this.inputFileUpdatedAt = hash.inputFileUpdatedAt
|
||||||
|
|
||||||
this.trackerUrls = hash.trackerUrls
|
this.trackerUrls = hash.trackerUrls
|
||||||
|
|
||||||
this.buildLikeAndDislikePercents()
|
this.buildLikeAndDislikePercents()
|
||||||
|
|
|
@ -26,6 +26,7 @@ export class Video implements VideoServerModel {
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
publishedAt: Date
|
publishedAt: Date
|
||||||
originallyPublishedAt: Date | string
|
originallyPublishedAt: Date | string
|
||||||
|
|
||||||
category: VideoConstant<number>
|
category: VideoConstant<number>
|
||||||
licence: VideoConstant<number>
|
licence: VideoConstant<number>
|
||||||
language: VideoConstant<string>
|
language: VideoConstant<string>
|
||||||
|
|
|
@ -48,6 +48,8 @@ class EndCard extends Component {
|
||||||
suspendedMessage: HTMLElement
|
suspendedMessage: HTMLElement
|
||||||
nextButton: HTMLElement
|
nextButton: HTMLElement
|
||||||
|
|
||||||
|
private timeout: any
|
||||||
|
|
||||||
private onEndedHandler: () => void
|
private onEndedHandler: () => void
|
||||||
private onPlayingHandler: () => 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.onEndedHandler) this.player().off([ 'auto-stopped', 'ended' ], this.onEndedHandler)
|
||||||
if (this.onPlayingHandler) this.player().off('playing', this.onPlayingHandler)
|
if (this.onPlayingHandler) this.player().off('playing', this.onPlayingHandler)
|
||||||
|
|
||||||
|
if (this.timeout) clearTimeout(this.timeout)
|
||||||
|
|
||||||
super.dispose()
|
super.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,8 +118,6 @@ class EndCard extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
showCard (cb: (canceled: boolean) => void) {
|
showCard (cb: (canceled: boolean) => void) {
|
||||||
let timeout: any
|
|
||||||
|
|
||||||
this.autoplayRing.setAttribute('stroke-dasharray', `${this.dashOffsetStart}`)
|
this.autoplayRing.setAttribute('stroke-dasharray', `${this.dashOffsetStart}`)
|
||||||
this.autoplayRing.setAttribute('stroke-dashoffset', `${-this.dashOffsetStart}`)
|
this.autoplayRing.setAttribute('stroke-dashoffset', `${-this.dashOffsetStart}`)
|
||||||
|
|
||||||
|
@ -126,17 +128,20 @@ class EndCard extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.upNextEvents.one('cancel', () => {
|
this.upNextEvents.one('cancel', () => {
|
||||||
clearTimeout(timeout)
|
clearTimeout(this.timeout)
|
||||||
|
this.timeout = undefined
|
||||||
cb(true)
|
cb(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.upNextEvents.one('playing', () => {
|
this.upNextEvents.one('playing', () => {
|
||||||
clearTimeout(timeout)
|
clearTimeout(this.timeout)
|
||||||
|
this.timeout = undefined
|
||||||
cb(true)
|
cb(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.upNextEvents.one('next', () => {
|
this.upNextEvents.one('next', () => {
|
||||||
clearTimeout(timeout)
|
clearTimeout(this.timeout)
|
||||||
|
this.timeout = undefined
|
||||||
cb(false)
|
cb(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -154,19 +159,20 @@ class EndCard extends Component {
|
||||||
this.suspendedMessage.innerText = this.options_.suspendedText
|
this.suspendedMessage.innerText = this.options_.suspendedText
|
||||||
goToPercent(0)
|
goToPercent(0)
|
||||||
this.ticks = 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) {
|
} else if (this.ticks >= this.totalTicks) {
|
||||||
clearTimeout(timeout)
|
clearTimeout(this.timeout)
|
||||||
|
this.timeout = undefined
|
||||||
cb(false)
|
cb(false)
|
||||||
} else {
|
} else {
|
||||||
this.suspendedMessage.innerText = ''
|
this.suspendedMessage.innerText = ''
|
||||||
tick()
|
tick()
|
||||||
timeout = setTimeout(update.bind(this), this.interval)
|
this.timeout = setTimeout(update.bind(this), this.interval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.container.style.display = 'block'
|
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 { VideoModel } from '@server/models/video/video'
|
||||||
import { VideoSourceModel } from '@server/models/video/video-source'
|
import { VideoSourceModel } from '@server/models/video/video-source'
|
||||||
import { MStreamingPlaylistFiles, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
|
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 { logger, loggerTagsFactory } from '../../../helpers/logger'
|
||||||
import {
|
import {
|
||||||
asyncMiddleware,
|
asyncMiddleware,
|
||||||
|
@ -121,7 +121,7 @@ async function replaceVideoSourceResumable (req: express.Request, res: express.R
|
||||||
|
|
||||||
await removeOldFiles({ video, files: oldWebVideoFiles, playlists: oldStreamingPlaylists })
|
await removeOldFiles({ video, files: oldWebVideoFiles, playlists: oldStreamingPlaylists })
|
||||||
|
|
||||||
await VideoSourceModel.create({
|
const source = await VideoSourceModel.create({
|
||||||
filename: originalFilename,
|
filename: originalFilename,
|
||||||
videoId: video.id,
|
videoId: video.id,
|
||||||
createdAt: inputFileUpdatedAt
|
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 })
|
Hooks.runAction('action:api.video.file-updated', { video, req, res })
|
||||||
|
|
||||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
return res.json(source.toFormattedJSON())
|
||||||
} finally {
|
} finally {
|
||||||
videoFileMutexReleaser()
|
videoFileMutexReleaser()
|
||||||
}
|
}
|
||||||
|
|
|
@ -462,7 +462,7 @@ export class VideosCommand extends AbstractCommand {
|
||||||
path: string
|
path: string
|
||||||
attributes: { fixture?: string } & { [id: string]: any }
|
attributes: { fixture?: string } & { [id: string]: any }
|
||||||
}): Promise<VideoCreateResult> {
|
}): Promise<VideoCreateResult> {
|
||||||
const { path, attributes, expectedStatus } = options
|
const { path, attributes, expectedStatus = HttpStatusCode.OK_200 } = options
|
||||||
|
|
||||||
let size = 0
|
let size = 0
|
||||||
let videoFilePath: string
|
let videoFilePath: string
|
||||||
|
@ -597,43 +597,47 @@ export class VideosCommand extends AbstractCommand {
|
||||||
const readable = createReadStream(videoFilePath, { highWaterMark: 8 * 1024 })
|
const readable = createReadStream(videoFilePath, { highWaterMark: 8 * 1024 })
|
||||||
return new Promise<GotResponse<{ video: VideoCreateResult }>>((resolve, reject) => {
|
return new Promise<GotResponse<{ video: VideoCreateResult }>>((resolve, reject) => {
|
||||||
readable.on('data', async function onData (chunk) {
|
readable.on('data', async function onData (chunk) {
|
||||||
readable.pause()
|
try {
|
||||||
|
readable.pause()
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
'Authorization': 'Bearer ' + token,
|
'Authorization': 'Bearer ' + token,
|
||||||
'Content-Type': 'application/octet-stream',
|
'Content-Type': 'application/octet-stream',
|
||||||
'Content-Range': contentRangeBuilder
|
'Content-Range': contentRangeBuilder
|
||||||
? contentRangeBuilder(start, chunk)
|
? contentRangeBuilder(start, chunk)
|
||||||
: `bytes ${start}-${start + chunk.length - 1}/${size}`,
|
: `bytes ${start}-${start + chunk.length - 1}/${size}`,
|
||||||
'Content-Length': contentLength ? contentLength + '' : chunk.length + ''
|
'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,
|
...options,
|
||||||
|
|
||||||
path: '/api/v1/videos/' + options.videoId + '/source/replace-resumable',
|
path: '/api/v1/videos/' + options.videoId + '/source/replace-resumable',
|
||||||
attributes: { fixture: options.fixture },
|
attributes: { fixture: options.fixture }
|
||||||
expectedStatus: HttpStatusCode.NO_CONTENT_204
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue