]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts
Fix live/upload redirection
[github/Chocobozzz/PeerTube.git] / client / src / app / +videos / +video-edit / video-add-components / video-upload.component.ts
CommitLineData
d4a8e7a6
C
1import { UploadState, UploadxOptions, UploadxService } from 'ngx-uploadx'
2import { HttpErrorResponse, HttpEventType, HttpHeaders } from '@angular/common/http'
2e257e36 3import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
fbad87b0 4import { Router } from '@angular/router'
2e257e36 5import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService, UserService } from '@app/core'
d4a8e7a6 6import { genericUploadErrorHandler, scrollToTop } from '@app/helpers'
67ed6552 7import { FormValidatorService } from '@app/shared/shared-forms'
d4a8e7a6 8import { BytesPipe, Video, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
fbad87b0 9import { LoadingBarService } from '@ngx-loading-bar/core'
2e80d256 10import { HttpStatusCode, VideoCreateResult, VideoPrivacy } from '@shared/models'
d4a8e7a6 11import { UploaderXFormData } from './uploaderx-form-data'
1942f11d 12import { VideoSend } from './video-send'
fbad87b0
C
13
14@Component({
15 selector: 'my-video-upload',
16 templateUrl: './video-upload.component.html',
17 styleUrls: [
78848714 18 '../shared/video-edit.component.scss',
457bb213
C
19 './video-upload.component.scss',
20 './video-send.scss'
fbad87b0
C
21 ]
22})
f6d6e7f8 23export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, AfterViewInit, CanComponentDeactivate {
fbad87b0 24 @Output() firstStepDone = new EventEmitter<string>()
7373507f 25 @Output() firstStepError = new EventEmitter<void>()
2f5d2ec5 26 @ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement>
fbad87b0 27
43620009 28 userVideoQuotaUsed = 0
bee0abff 29 userVideoQuotaUsedDaily = 0
43620009 30
7b992a86 31 isUploadingAudioFile = false
fbad87b0 32 isUploadingVideo = false
7b992a86 33
fbad87b0 34 videoUploaded = false
fbad87b0 35 videoUploadPercents = 0
2e80d256 36 videoUploadedIds: VideoCreateResult = {
fbad87b0 37 id: 0,
2e80d256
C
38 uuid: '',
39 shortUUID: ''
fbad87b0 40 }
d4132d3f 41 formData: FormData
7b992a86 42
7b992a86 43 previewfileUpload: File
fbad87b0 44
7373507f 45 error: string
d4132d3f 46 enableRetryAfterError: boolean
fbad87b0 47
3ce48a0c
C
48 schedulePublicationPossible = false
49
f6d6e7f8 50 // So that it can be accessed in the template
f6d6e7f8 51 protected readonly BASE_VIDEO_UPLOAD_URL = VideoService.BASE_VIDEO_URL + 'upload-resumable'
52
53 private uploadxOptions: UploadxOptions
54 private isUpdatingVideo = false
55 private fileToUpload: File
fbad87b0
C
56
57 constructor (
58 protected formValidatorService: FormValidatorService,
43620009 59 protected loadingBar: LoadingBarService,
f8b2c1b4 60 protected notifier: Notifier,
43620009
C
61 protected authService: AuthService,
62 protected serverService: ServerService,
63 protected videoService: VideoService,
64 protected videoCaptionService: VideoCaptionService,
fbad87b0 65 private userService: UserService,
2e257e36 66 private router: Router,
f6d6e7f8 67 private hooks: HooksService,
68 private resumableUploadService: UploadxService
69 ) {
fbad87b0 70 super()
f6d6e7f8 71
72 this.uploadxOptions = {
73 endpoint: this.BASE_VIDEO_UPLOAD_URL,
74 multiple: false,
75 token: this.authService.getAccessToken(),
76 uploaderClass: UploaderXFormData,
77 retryConfig: {
78 maxAttempts: 6,
79 shouldRetry: (code: number) => {
80 return code < 400 || code >= 501
81 }
82 }
83 }
fbad87b0
C
84 }
85
86 get videoExtensions () {
758f0d19 87 return this.serverConfig.video.file.extensions.join(', ')
fbad87b0
C
88 }
89
3ce48a0c
C
90 ngOnInit () {
91 super.ngOnInit()
92
93 this.userService.getMyVideoQuotaUsed()
94 .subscribe(data => {
95 this.userVideoQuotaUsed = data.videoQuotaUsed
96 this.userVideoQuotaUsedDaily = data.videoQuotaUsedDaily
97 })
98
99 this.resumableUploadService.events
100 .subscribe(state => this.onUploadVideoOngoing(state))
101
102 this.schedulePublicationPossible = this.videoPrivacies.some(p => p.id === VideoPrivacy.PRIVATE)
103 }
104
105 ngAfterViewInit () {
106 this.hooks.runAction('action:video-upload.init', 'video-edit')
107 }
108
109 ngOnDestroy () {
110 this.cancelUpload()
111 }
112
113 canDeactivate () {
114 let text = ''
115
116 if (this.videoUploaded === true) {
117 // FIXME: cannot concatenate strings using $localize
118 text = $localize`Your video was uploaded to your account and is private.` + ' ' +
119 $localize`But associated data (tags, description...) will be lost, are you sure you want to leave this page?`
120 } else {
121 text = $localize`Your video is not uploaded yet, are you sure you want to leave this page?`
122 }
123
124 return {
125 canDeactivate: !this.isUploadingVideo,
126 text
127 }
128 }
129
f6d6e7f8 130 onUploadVideoOngoing (state: UploadState) {
131 switch (state.status) {
9df52d66 132 case 'error': {
f6d6e7f8 133 const error = state.response?.error || 'Unknow error'
134
135 this.handleUploadError({
136 error: new Error(error),
137 name: 'HttpErrorResponse',
138 message: error,
139 ok: false,
140 headers: new HttpHeaders(state.responseHeaders),
141 status: +state.responseStatus,
142 statusText: error,
143 type: HttpEventType.Response,
144 url: state.url
145 })
146 break
9df52d66 147 }
f6d6e7f8 148
149 case 'cancelled':
150 this.isUploadingVideo = false
151 this.videoUploadPercents = 0
152
153 this.firstStepError.emit()
154 this.enableRetryAfterError = false
155 this.error = ''
156 break
157
158 case 'queue':
159 this.closeFirstStep(state.name)
160 break
161
162 case 'uploading':
163 this.videoUploadPercents = state.progress
164 break
165
166 case 'paused':
71fb8b5a 167 this.notifier.info($localize`Upload on hold`)
f6d6e7f8 168 break
169
170 case 'complete':
171 this.videoUploaded = true
172 this.videoUploadPercents = 100
173
174 this.videoUploadedIds = state?.response.video
175 break
176 }
177 }
178
f6d6e7f8 179 onFileDropped (files: FileList) {
c9ff8a08 180 this.videofileInput.nativeElement.files = files
7b992a86 181
f6d6e7f8 182 this.onFileChange({ target: this.videofileInput.nativeElement })
7b992a86
C
183 }
184
f6d6e7f8 185 onFileChange (event: Event | { target: HTMLInputElement }) {
186 const file = (event.target as HTMLInputElement).files[0]
e713698f 187
f6d6e7f8 188 if (!file) return
e713698f 189
f6d6e7f8 190 if (!this.checkGlobalUserQuota(file)) return
191 if (!this.checkDailyUserQuota(file)) return
fbad87b0 192
f6d6e7f8 193 if (this.isAudioFile(file.name)) {
7b992a86 194 this.isUploadingAudioFile = true
bee0abff
FA
195 return
196 }
197
fbad87b0 198 this.isUploadingVideo = true
f6d6e7f8 199 this.fileToUpload = file
fbad87b0 200
f6d6e7f8 201 this.uploadFile(file)
d4132d3f
RK
202 }
203
f6d6e7f8 204 uploadAudio () {
205 this.uploadFile(this.getInputVideoFile(), this.previewfileUpload)
206 }
fbad87b0 207
f6d6e7f8 208 retryUpload () {
209 this.enableRetryAfterError = false
210 this.error = ''
211 this.uploadFile(this.fileToUpload)
212 }
f2eb23cd 213
f6d6e7f8 214 cancelUpload () {
215 this.resumableUploadService.control({ action: 'cancel' })
fbad87b0
C
216 }
217
59c9c5d9
C
218 isPublishingButtonDisabled () {
219 return !this.form.valid ||
220 this.isUpdatingVideo === true ||
c7a53f61
C
221 this.videoUploaded !== true ||
222 !this.videoUploadedIds.id
59c9c5d9
C
223 }
224
f6d6e7f8 225 getAudioUploadLabel () {
226 const videofile = this.getInputVideoFile()
227 if (!videofile) return $localize`Upload`
228
229 return $localize`Upload ${videofile.name}`
230 }
231
fbad87b0 232 updateSecondStep () {
c7a53f61 233 if (this.isPublishingButtonDisabled() || !this.checkForm()) {
fbad87b0
C
234 return
235 }
236
237 const video = new VideoEdit()
238 video.patch(this.form.value)
239 video.id = this.videoUploadedIds.id
240 video.uuid = this.videoUploadedIds.uuid
2e80d256 241 video.shortUUID = this.videoUploadedIds.shortUUID
fbad87b0
C
242
243 this.isUpdatingVideo = true
43620009
C
244
245 this.updateVideoAndCaptions(video)
1378c0d3
C
246 .subscribe({
247 next: () => {
fbad87b0
C
248 this.isUpdatingVideo = false
249 this.isUploadingVideo = false
fbad87b0 250
66357162 251 this.notifier.success($localize`Video published.`)
d4a8e7a6 252 this.router.navigateByUrl(Video.buildWatchUrl(video))
fbad87b0
C
253 },
254
1378c0d3 255 error: err => {
7373507f
C
256 this.error = err.message
257 scrollToTop()
fbad87b0
C
258 console.error(err)
259 }
1378c0d3 260 })
fbad87b0 261 }
7b992a86 262
f6d6e7f8 263 private getInputVideoFile () {
264 return this.videofileInput.nativeElement.files[0]
265 }
266
267 private uploadFile (file: File, previewfile?: File) {
268 const metadata = {
269 waitTranscoding: true,
270 commentsEnabled: true,
271 downloadEnabled: true,
272 channelId: this.firstStepChannelId,
273 nsfw: this.serverConfig.instance.isNSFW,
a3f45a2a 274 privacy: this.highestPrivacy.toString(),
f6d6e7f8 275 filename: file.name,
276 previewfile: previewfile as any
277 }
278
279 this.resumableUploadService.handleFiles(file, {
280 ...this.uploadxOptions,
281 metadata
282 })
283
284 this.isUploadingVideo = true
285 }
286
287 private handleUploadError (err: HttpErrorResponse) {
288 // Reset progress (but keep isUploadingVideo true)
289 this.videoUploadPercents = 0
290 this.enableRetryAfterError = true
291
292 this.error = genericUploadErrorHandler({
293 err,
294 name: $localize`video`,
295 notifier: this.notifier,
296 sticky: false
297 })
298
299 if (err.status === HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415) {
300 this.cancelUpload()
301 }
302 }
303
304 private closeFirstStep (filename: string) {
305 const nameWithoutExtension = filename.replace(/\.[^/.]+$/, '')
306 const name = nameWithoutExtension.length < 3 ? filename : nameWithoutExtension
307
308 this.form.patchValue({
309 name,
310 privacy: this.firstStepPrivacyId,
311 nsfw: this.serverConfig.instance.isNSFW,
312 channelId: this.firstStepChannelId,
313 previewfile: this.previewfileUpload
314 })
315
316 this.firstStepDone.emit(name)
317 }
318
7b992a86
C
319 private checkGlobalUserQuota (videofile: File) {
320 const bytePipes = new BytesPipe()
321
322 // Check global user quota
323 const videoQuota = this.authService.getUser().videoQuota
324 if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) {
66357162
C
325 const videoSizeBytes = bytePipes.transform(videofile.size, 0)
326 const videoQuotaUsedBytes = bytePipes.transform(this.userVideoQuotaUsed, 0)
327 const videoQuotaBytes = bytePipes.transform(videoQuota, 0)
328
9df52d66 329 // eslint-disable-next-line max-len
f6d6e7f8 330 const msg = $localize`Your video quota is exceeded with this video (video size: ${videoSizeBytes}, used: ${videoQuotaUsedBytes}, quota: ${videoQuotaBytes})`
7b992a86
C
331 this.notifier.error(msg)
332
333 return false
334 }
335
336 return true
337 }
338
339 private checkDailyUserQuota (videofile: File) {
340 const bytePipes = new BytesPipe()
341
342 // Check daily user quota
343 const videoQuotaDaily = this.authService.getUser().videoQuotaDaily
344 if (videoQuotaDaily !== -1 && (this.userVideoQuotaUsedDaily + videofile.size) > videoQuotaDaily) {
66357162
C
345 const videoSizeBytes = bytePipes.transform(videofile.size, 0)
346 const quotaUsedDailyBytes = bytePipes.transform(this.userVideoQuotaUsedDaily, 0)
347 const quotaDailyBytes = bytePipes.transform(videoQuotaDaily, 0)
9df52d66 348 // eslint-disable-next-line max-len
f6d6e7f8 349 const msg = $localize`Your daily video quota is exceeded with this video (video size: ${videoSizeBytes}, used: ${quotaUsedDailyBytes}, quota: ${quotaDailyBytes})`
7b992a86
C
350 this.notifier.error(msg)
351
352 return false
353 }
354
355 return true
356 }
357
358 private isAudioFile (filename: string) {
99d362de
C
359 const extensions = [ '.mp3', '.flac', '.ogg', '.wma', '.wav' ]
360
361 return extensions.some(e => filename.endsWith(e))
7b992a86 362 }
fbad87b0 363}