From fbad87b0472f574409f7aa3ae7f8b54927d0cdd6 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 2 Aug 2018 15:34:09 +0200 Subject: Add ability to import video with youtube-dl --- .../videos/+video-edit/video-add.component.html | 68 +----- .../videos/+video-edit/video-add.component.scss | 117 +++------- .../app/videos/+video-edit/video-add.component.ts | 252 ++------------------- .../src/app/videos/+video-edit/video-add.module.ts | 6 +- .../videos/+video-edit/video-import.component.html | 55 +++++ .../videos/+video-edit/video-import.component.scss | 37 +++ .../videos/+video-edit/video-import.component.ts | 161 +++++++++++++ .../videos/+video-edit/video-update.component.ts | 1 - .../videos/+video-edit/video-upload.component.html | 58 +++++ .../videos/+video-edit/video-upload.component.scss | 85 +++++++ .../videos/+video-edit/video-upload.component.ts | 251 ++++++++++++++++++++ 11 files changed, 712 insertions(+), 379 deletions(-) create mode 100644 client/src/app/videos/+video-edit/video-import.component.html create mode 100644 client/src/app/videos/+video-edit/video-import.component.scss create mode 100644 client/src/app/videos/+video-edit/video-import.component.ts create mode 100644 client/src/app/videos/+video-edit/video-upload.component.html create mode 100644 client/src/app/videos/+video-edit/video-upload.component.scss create mode 100644 client/src/app/videos/+video-edit/video-upload.component.ts (limited to 'client/src/app/videos/+video-edit') diff --git a/client/src/app/videos/+video-edit/video-add.component.html b/client/src/app/videos/+video-edit/video-add.component.html index 9c2c01c65..ed8d91c11 100644 --- a/client/src/app/videos/+video-edit/video-add.component.html +++ b/client/src/app/videos/+video-edit/video-add.component.html @@ -1,65 +1,17 @@
- Upload your video - Upload {{ videoFileName }} + Import {{ videoName }} + Upload {{ videoName }}
-
-
-
+ -
- Select the file to upload - -
- (.mp4, .webm, .ogv) + + + -
- -
- -
-
- -
- -
- -
-
-
-
- -
- - -
- - -
- - -
-
Publish will be available when upload is finished
- -
- - -
-
-
+ + + +
diff --git a/client/src/app/videos/+video-edit/video-add.component.scss b/client/src/app/videos/+video-edit/video-add.component.scss index c0b5f3d07..a811b9cf0 100644 --- a/client/src/app/videos/+video-edit/video-add.component.scss +++ b/client/src/app/videos/+video-edit/video-add.component.scss @@ -1,101 +1,54 @@ @import '_variables'; @import '_mixins'; -.upload-video-container { - border-radius: 3px; - background-color: #F7F7F7; - border: 3px solid #EAEAEA; - width: 100%; - height: 440px; - margin-top: 40px; - display: flex; - justify-content: center; - align-items: center; +$border-width: 3px; +$border-type: solid; +$border-color: #EAEAEA; - .peertube-select-container { - @include peertube-select-container(190px); - } - - .upload-video { - display: flex; - flex-direction: column; - align-items: center; - - .form-group-channel { - margin-bottom: 20px; - } - - .icon.icon-upload { - @include icon(90px); - margin-bottom: 25px; - cursor: default; - - background-image: url('../../../assets/images/video/upload.svg'); - } - - .button-file { - @include peertube-button-file(auto); - - min-width: 190px; - } +$background-color: #F7F7F7; - .button-file-extension { - display: block; - font-size: 12px; - margin-top: 5px; - } - } - - .form-group-channel { - margin-top: 35px; +/deep/ tabset.root-tabset.video-add-tabset { + &.hide-nav .nav { + display: none !important; } -} -.upload-progress-cancel { - display: flex; - margin-top: 25px; - margin-bottom: 40px; + & > .nav { - p-progressBar { - flex-grow: 1; - - /deep/ .ui-progressbar { - font-size: 15px !important; - color: #fff !important; - height: 30px !important; - line-height: 30px !important; - border-radius: 3px !important; - background-color: rgba(11, 204, 41, 0.16) !important; - - .ui-progressbar-value { - background-color: #0BCC29 !important; - } + border-bottom: $border-width $border-type $border-color; + margin: 0 !important; - .ui-progressbar-label { - text-align: left; - padding-left: 18px; - margin-top: 0 !important; - } + & > li { + margin-bottom: -$border-width; } - &.processing { - /deep/ .ui-progressbar-label { - // Same color as background to hide "100%" - color: rgba(11, 204, 41, 0.16) !important; + .nav-link { + height: 40px !important; + padding: 0 30px !important; + font-size: 15px; + + &.active { + border: $border-width $border-type $border-color; + border-bottom: none; + background-color: $background-color !important; - &::before { - content: 'Processing...'; - color: #fff; + span { + border-bottom: 2px solid #F1680D; + font-weight: $font-bold; } } } } - input { - @include peertube-button; - @include grey-button; + .upload-video-container { + border: $border-width $border-type $border-color; + border-top: none; - margin-left: 10px; + background-color: $background-color; + border-radius: 3px; + width: 100%; + height: 440px; + display: flex; + justify-content: center; + align-items: center; } -} - +} \ No newline at end of file diff --git a/client/src/app/videos/+video-edit/video-add.component.ts b/client/src/app/videos/+video-edit/video-add.component.ts index 651ee8dd2..64071b40c 100644 --- a/client/src/app/videos/+video-edit/video-add.component.ts +++ b/client/src/app/videos/+video-edit/video-add.component.ts @@ -1,251 +1,29 @@ -import { HttpEventType, HttpResponse } from '@angular/common/http' -import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core' -import { Router } from '@angular/router' -import { UserService } from '@app/shared' +import { Component, ViewChild } from '@angular/core' import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service' -import { LoadingBarService } from '@ngx-loading-bar/core' -import { NotificationsService } from 'angular2-notifications' -import { BytesPipe } from 'ngx-pipes' -import { Subscription } from 'rxjs' -import { VideoConstant, VideoPrivacy } from '../../../../../shared/models/videos' -import { AuthService, ServerService } from '../../core' -import { FormReactive } from '../../shared' -import { populateAsyncUserVideoChannels } from '../../shared/misc/utils' -import { VideoEdit } from '../../shared/video/video-edit.model' -import { VideoService } from '../../shared/video/video.service' -import { I18n } from '@ngx-translate/i18n-polyfill' -import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' -import { switchMap } from 'rxjs/operators' -import { VideoCaptionService } from '@app/shared/video-caption' -import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' +import { VideoImportComponent } from '@app/videos/+video-edit/video-import.component' +import { VideoUploadComponent } from '@app/videos/+video-edit/video-upload.component' @Component({ selector: 'my-videos-add', templateUrl: './video-add.component.html', - styleUrls: [ - './shared/video-edit.component.scss', - './video-add.component.scss' - ] + styleUrls: [ './video-add.component.scss' ] }) -export class VideoAddComponent extends FormReactive implements OnInit, OnDestroy, CanComponentDeactivate { - @ViewChild('videofileInput') videofileInput +export class VideoAddComponent implements CanComponentDeactivate { + @ViewChild('videoUpload') videoUpload: VideoUploadComponent + @ViewChild('videoImport') videoImport: VideoImportComponent - // So that it can be accessed in the template - readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY + secondStepType: 'upload' | 'import' + videoName: string - isUploadingVideo = false - isUpdatingVideo = false - videoUploaded = false - videoUploadObservable: Subscription = null - videoUploadPercents = 0 - videoUploadedIds = { - id: 0, - uuid: '' - } - videoFileName: string - - userVideoChannels: { id: number, label: string, support: string }[] = [] - userVideoQuotaUsed = 0 - videoPrivacies: VideoConstant[] = [] - firstStepPrivacyId = 0 - firstStepChannelId = 0 - videoCaptions: VideoCaptionEdit[] = [] - - constructor ( - protected formValidatorService: FormValidatorService, - private router: Router, - private notificationsService: NotificationsService, - private authService: AuthService, - private userService: UserService, - private serverService: ServerService, - private videoService: VideoService, - private loadingBar: LoadingBarService, - private i18n: I18n, - private videoCaptionService: VideoCaptionService - ) { - super() - } - - get videoExtensions () { - return this.serverService.getConfig().video.file.extensions.join(',') - } - - ngOnInit () { - this.buildForm({}) - - populateAsyncUserVideoChannels(this.authService, this.userVideoChannels) - .then(() => this.firstStepChannelId = this.userVideoChannels[0].id) - - this.userService.getMyVideoQuotaUsed() - .subscribe(data => this.userVideoQuotaUsed = data.videoQuotaUsed) - - this.serverService.videoPrivaciesLoaded - .subscribe( - () => { - this.videoPrivacies = this.serverService.getVideoPrivacies() - - // Public by default - this.firstStepPrivacyId = VideoPrivacy.PUBLIC - }) - } - - ngOnDestroy () { - if (this.videoUploadObservable) { - this.videoUploadObservable.unsubscribe() - } + onFirstStepDone (type: 'upload' | 'import', videoName: string) { + this.secondStepType = type + this.videoName = videoName } canDeactivate () { - let text = '' - - if (this.videoUploaded === true) { - // FIXME: cannot concatenate strings inside i18n service :/ - text = this.i18n('Your video was uploaded in your account and is private.') + - this.i18n('But associated data (tags, description...) will be lost, are you sure you want to leave this page?') - } else { - text = this.i18n('Your video is not uploaded yet, are you sure you want to leave this page?') - } - - return { - canDeactivate: !this.isUploadingVideo, - text - } - } - - fileChange () { - this.uploadFirstStep() - } - - checkForm () { - this.forceCheck() - - return this.form.valid - } - - cancelUpload () { - if (this.videoUploadObservable !== null) { - this.videoUploadObservable.unsubscribe() - this.isUploadingVideo = false - this.videoUploadPercents = 0 - this.videoUploadObservable = null - this.notificationsService.info(this.i18n('Info'), this.i18n('Upload cancelled')) - } - } - - uploadFirstStep () { - const videofile = this.videofileInput.nativeElement.files[0] as File - if (!videofile) return - - // Cannot upload videos > 8GB for now - if (videofile.size > 8 * 1024 * 1024 * 1024) { - this.notificationsService.error(this.i18n('Error'), this.i18n('We are sorry but PeerTube cannot handle videos > 8GB')) - return - } - - const videoQuota = this.authService.getUser().videoQuota - if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) { - const bytePipes = new BytesPipe() - - const msg = this.i18n( - 'Your video quota is exceeded with this video (video size: {{ videoSize }}, used: {{ videoQuotaUsed }}, quota: {{ videoQuota }})', - { - videoSize: bytePipes.transform(videofile.size, 0), - videoQuotaUsed: bytePipes.transform(this.userVideoQuotaUsed, 0), - videoQuota: bytePipes.transform(videoQuota, 0) - } - ) - this.notificationsService.error(this.i18n('Error'), msg) - return - } - - this.videoFileName = videofile.name - - const nameWithoutExtension = videofile.name.replace(/\.[^/.]+$/, '') - let name: string - - // If the name of the file is very small, keep the extension - if (nameWithoutExtension.length < 3) name = videofile.name - else name = nameWithoutExtension - - const privacy = this.firstStepPrivacyId.toString() - const nsfw = false - const waitTranscoding = true - const commentsEnabled = true - const channelId = this.firstStepChannelId.toString() - - const formData = new FormData() - formData.append('name', name) - // Put the video "private" -> we are waiting the user validation of the second step - formData.append('privacy', VideoPrivacy.PRIVATE.toString()) - formData.append('nsfw', '' + nsfw) - formData.append('commentsEnabled', '' + commentsEnabled) - formData.append('waitTranscoding', '' + waitTranscoding) - formData.append('channelId', '' + channelId) - formData.append('videofile', videofile) - - this.isUploadingVideo = true - this.form.patchValue({ - name, - privacy, - nsfw, - channelId - }) - - this.videoUploadObservable = this.videoService.uploadVideo(formData).subscribe( - event => { - if (event.type === HttpEventType.UploadProgress) { - this.videoUploadPercents = Math.round(100 * event.loaded / event.total) - } else if (event instanceof HttpResponse) { - this.videoUploaded = true - - this.videoUploadedIds = event.body.video - - this.videoUploadObservable = null - } - }, - - err => { - // Reset progress - this.isUploadingVideo = false - this.videoUploadPercents = 0 - this.videoUploadObservable = null - this.notificationsService.error(this.i18n('Error'), err.message) - } - ) - } - - updateSecondStep () { - if (this.checkForm() === false) { - return - } - - const video = new VideoEdit() - video.patch(this.form.value) - video.id = this.videoUploadedIds.id - video.uuid = this.videoUploadedIds.uuid - - this.isUpdatingVideo = true - this.loadingBar.start() - this.videoService.updateVideo(video) - .pipe( - // Then update captions - switchMap(() => this.videoCaptionService.updateCaptions(video.id, this.videoCaptions)) - ) - .subscribe( - () => { - this.isUpdatingVideo = false - this.isUploadingVideo = false - this.loadingBar.complete() - - this.notificationsService.success(this.i18n('Success'), this.i18n('Video published.')) - this.router.navigate([ '/videos/watch', video.uuid ]) - }, + if (this.secondStepType === 'upload') return this.videoUpload.canDeactivate() + if (this.secondStepType === 'import') return this.videoImport.canDeactivate() - err => { - this.isUpdatingVideo = false - this.notificationsService.error(this.i18n('Error'), err.message) - console.error(err) - } - ) + return { canDeactivate: true } } } diff --git a/client/src/app/videos/+video-edit/video-add.module.ts b/client/src/app/videos/+video-edit/video-add.module.ts index 1bfedf251..91f544971 100644 --- a/client/src/app/videos/+video-edit/video-add.module.ts +++ b/client/src/app/videos/+video-edit/video-add.module.ts @@ -5,6 +5,8 @@ import { VideoEditModule } from './shared/video-edit.module' import { VideoAddRoutingModule } from './video-add-routing.module' import { VideoAddComponent } from './video-add.component' import { CanDeactivateGuard } from '../../shared/guards/can-deactivate-guard.service' +import { VideoUploadComponent } from '@app/videos/+video-edit/video-upload.component' +import { VideoImportComponent } from '@app/videos/+video-edit/video-import.component' @NgModule({ imports: [ @@ -14,7 +16,9 @@ import { CanDeactivateGuard } from '../../shared/guards/can-deactivate-guard.ser ProgressBarModule ], declarations: [ - VideoAddComponent + VideoAddComponent, + VideoUploadComponent, + VideoImportComponent ], exports: [ VideoAddComponent diff --git a/client/src/app/videos/+video-edit/video-import.component.html b/client/src/app/videos/+video-edit/video-import.component.html new file mode 100644 index 000000000..9d71a0717 --- /dev/null +++ b/client/src/app/videos/+video-edit/video-import.component.html @@ -0,0 +1,55 @@ +
+
+
+ +
+ + +
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ + +
+
+ +
+ Congratulations, the video behind {{ targetUrl }} will be imported! You can already add information about this video. +
+ + +
+ + +
+
+ + +
+
+
diff --git a/client/src/app/videos/+video-edit/video-import.component.scss b/client/src/app/videos/+video-edit/video-import.component.scss new file mode 100644 index 000000000..9ada9db19 --- /dev/null +++ b/client/src/app/videos/+video-edit/video-import.component.scss @@ -0,0 +1,37 @@ +@import '_variables'; +@import '_mixins'; + +$width-size: 190px; + +.peertube-select-container { + @include peertube-select-container($width-size); +} + +.import-video { + display: flex; + flex-direction: column; + align-items: center; + + .icon.icon-upload { + @include icon(90px); + margin-bottom: 25px; + cursor: default; + + background-image: url('../../../assets/images/video/upload.svg'); + } + + input[type=text] { + @include peertube-input-text($width-size); + display: block; + } + + input[type=button] { + @include peertube-button; + @include orange-button; + + width: $width-size; + margin-top: 30px; + } +} + + diff --git a/client/src/app/videos/+video-edit/video-import.component.ts b/client/src/app/videos/+video-edit/video-import.component.ts new file mode 100644 index 000000000..bd4482e17 --- /dev/null +++ b/client/src/app/videos/+video-edit/video-import.component.ts @@ -0,0 +1,161 @@ +import { Component, EventEmitter, OnInit, Output } from '@angular/core' +import { Router } from '@angular/router' +import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service' +import { NotificationsService } from 'angular2-notifications' +import { VideoConstant, VideoPrivacy, VideoUpdate } from '../../../../../shared/models/videos' +import { AuthService, ServerService } from '../../core' +import { FormReactive } from '../../shared' +import { populateAsyncUserVideoChannels } from '../../shared/misc/utils' +import { VideoService } from '../../shared/video/video.service' +import { I18n } from '@ngx-translate/i18n-polyfill' +import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' +import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' +import { VideoImportService } from '@app/shared/video-import' +import { VideoEdit } from '@app/shared/video/video-edit.model' +import { switchMap } from 'rxjs/operators' +import { LoadingBarService } from '@ngx-loading-bar/core' +import { VideoCaptionService } from '@app/shared/video-caption' + +@Component({ + selector: 'my-video-import', + templateUrl: './video-import.component.html', + styleUrls: [ + './shared/video-edit.component.scss', + './video-import.component.scss' + ] +}) +export class VideoImportComponent extends FormReactive implements OnInit, CanComponentDeactivate { + @Output() firstStepDone = new EventEmitter() + + targetUrl = '' + videoFileName: string + + isImportingVideo = false + hasImportedVideo = false + isUpdatingVideo = false + + userVideoChannels: { id: number, label: string, support: string }[] = [] + videoPrivacies: VideoConstant[] = [] + videoCaptions: VideoCaptionEdit[] = [] + + firstStepPrivacyId = 0 + firstStepChannelId = 0 + video: VideoEdit + + constructor ( + protected formValidatorService: FormValidatorService, + private router: Router, + private loadingBar: LoadingBarService, + private notificationsService: NotificationsService, + private authService: AuthService, + private serverService: ServerService, + private videoService: VideoService, + private videoImportService: VideoImportService, + private videoCaptionService: VideoCaptionService, + private i18n: I18n + ) { + super() + } + + ngOnInit () { + this.buildForm({}) + + populateAsyncUserVideoChannels(this.authService, this.userVideoChannels) + .then(() => this.firstStepChannelId = this.userVideoChannels[ 0 ].id) + + this.serverService.videoPrivaciesLoaded + .subscribe( + () => { + this.videoPrivacies = this.serverService.getVideoPrivacies() + + // Private by default + this.firstStepPrivacyId = VideoPrivacy.PRIVATE + }) + } + + canDeactivate () { + return { canDeactivate: true } + } + + checkForm () { + this.forceCheck() + + return this.form.valid + } + + isTargetUrlValid () { + return this.targetUrl && this.targetUrl.match(/https?:\/\//) + } + + importVideo () { + this.isImportingVideo = true + + const videoUpdate: VideoUpdate = { + privacy: this.firstStepPrivacyId, + waitTranscoding: false, + commentsEnabled: true, + channelId: this.firstStepChannelId + } + + this.videoImportService.importVideo(this.targetUrl, videoUpdate).subscribe( + res => { + this.firstStepDone.emit(res.video.name) + this.isImportingVideo = false + this.hasImportedVideo = true + + this.video = new VideoEdit(Object.assign(res.video, { + commentsEnabled: videoUpdate.commentsEnabled, + support: null, + thumbnailUrl: null, + previewUrl: null + })) + this.hydrateFormFromVideo() + }, + + err => { + this.isImportingVideo = false + this.notificationsService.error(this.i18n('Error'), err.message) + } + ) + } + + updateSecondStep () { + if (this.checkForm() === false) { + return + } + + this.video.patch(this.form.value) + + this.loadingBar.start() + this.isUpdatingVideo = true + + // Update the video + this.videoService.updateVideo(this.video) + .pipe( + // Then update captions + switchMap(() => this.videoCaptionService.updateCaptions(this.video.id, this.videoCaptions)) + ) + .subscribe( + () => { + this.isUpdatingVideo = false + this.loadingBar.complete() + this.notificationsService.success(this.i18n('Success'), this.i18n('Video to import updated.')) + + // TODO: route to imports list + // this.router.navigate([ '/videos/watch', this.video.uuid ]) + }, + + err => { + this.loadingBar.complete() + this.isUpdatingVideo = false + this.notificationsService.error(this.i18n('Error'), err.message) + console.error(err) + } + ) + + } + + private hydrateFormFromVideo () { + this.form.patchValue(this.video.toFormPatch()) + } +} diff --git a/client/src/app/videos/+video-edit/video-update.component.ts b/client/src/app/videos/+video-edit/video-update.component.ts index 798c48f3c..0c60e3439 100644 --- a/client/src/app/videos/+video-edit/video-update.component.ts +++ b/client/src/app/videos/+video-edit/video-update.component.ts @@ -126,7 +126,6 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { console.error(err) } ) - } private hydrateFormFromVideo () { diff --git a/client/src/app/videos/+video-edit/video-upload.component.html b/client/src/app/videos/+video-edit/video-upload.component.html new file mode 100644 index 000000000..8c0723155 --- /dev/null +++ b/client/src/app/videos/+video-edit/video-upload.component.html @@ -0,0 +1,58 @@ +
+
+
+ +
+ Select the file to upload + +
+ (.mp4, .webm, .ogv) + +
+ +
+ +
+
+ +
+ +
+ +
+
+
+
+ +
+ + +
+ + +
+ + +
+
Publish will be available when upload is finished
+ +
+ + +
+
+
\ No newline at end of file diff --git a/client/src/app/videos/+video-edit/video-upload.component.scss b/client/src/app/videos/+video-edit/video-upload.component.scss new file mode 100644 index 000000000..015835672 --- /dev/null +++ b/client/src/app/videos/+video-edit/video-upload.component.scss @@ -0,0 +1,85 @@ +@import '_variables'; +@import '_mixins'; + +.peertube-select-container { + @include peertube-select-container(190px); +} + +.upload-video { + display: flex; + flex-direction: column; + align-items: center; + + .form-group-channel { + margin-bottom: 20px; + margin-top: 35px; + } + + .icon.icon-upload { + @include icon(90px); + margin-bottom: 25px; + cursor: default; + + background-image: url('../../../assets/images/video/upload.svg'); + } + + .button-file { + @include peertube-button-file(auto); + + min-width: 190px; + } + + .button-file-extension { + display: block; + font-size: 12px; + margin-top: 5px; + } +} + +.upload-progress-cancel { + display: flex; + margin-top: 25px; + margin-bottom: 40px; + + p-progressBar { + flex-grow: 1; + + /deep/ .ui-progressbar { + font-size: 15px !important; + color: #fff !important; + height: 30px !important; + line-height: 30px !important; + border-radius: 3px !important; + background-color: rgba(11, 204, 41, 0.16) !important; + + .ui-progressbar-value { + background-color: #0BCC29 !important; + } + + .ui-progressbar-label { + text-align: left; + padding-left: 18px; + margin-top: 0 !important; + } + } + + &.processing { + /deep/ .ui-progressbar-label { + // Same color as background to hide "100%" + color: rgba(11, 204, 41, 0.16) !important; + + &::before { + content: 'Processing...'; + color: #fff; + } + } + } + } + + input { + @include peertube-button; + @include grey-button; + + margin-left: 10px; + } +} \ No newline at end of file diff --git a/client/src/app/videos/+video-edit/video-upload.component.ts b/client/src/app/videos/+video-edit/video-upload.component.ts new file mode 100644 index 000000000..e6c391d2f --- /dev/null +++ b/client/src/app/videos/+video-edit/video-upload.component.ts @@ -0,0 +1,251 @@ +import { HttpEventType, HttpResponse } from '@angular/common/http' +import { Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' +import { Router } from '@angular/router' +import { UserService } from '@app/shared' +import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service' +import { LoadingBarService } from '@ngx-loading-bar/core' +import { NotificationsService } from 'angular2-notifications' +import { BytesPipe } from 'ngx-pipes' +import { Subscription } from 'rxjs' +import { VideoConstant, VideoPrivacy } from '../../../../../shared/models/videos' +import { AuthService, ServerService } from '../../core' +import { FormReactive } from '../../shared' +import { populateAsyncUserVideoChannels } from '../../shared/misc/utils' +import { VideoEdit } from '../../shared/video/video-edit.model' +import { VideoService } from '../../shared/video/video.service' +import { I18n } from '@ngx-translate/i18n-polyfill' +import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' +import { switchMap } from 'rxjs/operators' +import { VideoCaptionService } from '@app/shared/video-caption' +import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' + +@Component({ + selector: 'my-video-upload', + templateUrl: './video-upload.component.html', + styleUrls: [ + './shared/video-edit.component.scss', + './video-upload.component.scss' + ] +}) +export class VideoUploadComponent extends FormReactive implements OnInit, OnDestroy, CanComponentDeactivate { + @Output() firstStepDone = new EventEmitter() + @ViewChild('videofileInput') videofileInput + + // So that it can be accessed in the template + readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY + + isUploadingVideo = false + isUpdatingVideo = false + videoUploaded = false + videoUploadObservable: Subscription = null + videoUploadPercents = 0 + videoUploadedIds = { + id: 0, + uuid: '' + } + + userVideoChannels: { id: number, label: string, support: string }[] = [] + userVideoQuotaUsed = 0 + videoPrivacies: VideoConstant[] = [] + firstStepPrivacyId = 0 + firstStepChannelId = 0 + videoCaptions: VideoCaptionEdit[] = [] + + constructor ( + protected formValidatorService: FormValidatorService, + private router: Router, + private notificationsService: NotificationsService, + private authService: AuthService, + private userService: UserService, + private serverService: ServerService, + private videoService: VideoService, + private loadingBar: LoadingBarService, + private i18n: I18n, + private videoCaptionService: VideoCaptionService + ) { + super() + } + + get videoExtensions () { + return this.serverService.getConfig().video.file.extensions.join(',') + } + + ngOnInit () { + this.buildForm({}) + + populateAsyncUserVideoChannels(this.authService, this.userVideoChannels) + .then(() => this.firstStepChannelId = this.userVideoChannels[0].id) + + this.userService.getMyVideoQuotaUsed() + .subscribe(data => this.userVideoQuotaUsed = data.videoQuotaUsed) + + this.serverService.videoPrivaciesLoaded + .subscribe( + () => { + this.videoPrivacies = this.serverService.getVideoPrivacies() + + // Public by default + this.firstStepPrivacyId = VideoPrivacy.PUBLIC + }) + } + + ngOnDestroy () { + if (this.videoUploadObservable) { + this.videoUploadObservable.unsubscribe() + } + } + + canDeactivate () { + let text = '' + + if (this.videoUploaded === true) { + // FIXME: cannot concatenate strings inside i18n service :/ + text = this.i18n('Your video was uploaded in your account and is private.') + + this.i18n('But associated data (tags, description...) will be lost, are you sure you want to leave this page?') + } else { + text = this.i18n('Your video is not uploaded yet, are you sure you want to leave this page?') + } + + return { + canDeactivate: !this.isUploadingVideo, + text + } + } + + fileChange () { + this.uploadFirstStep() + } + + checkForm () { + this.forceCheck() + + return this.form.valid + } + + cancelUpload () { + if (this.videoUploadObservable !== null) { + this.videoUploadObservable.unsubscribe() + this.isUploadingVideo = false + this.videoUploadPercents = 0 + this.videoUploadObservable = null + this.notificationsService.info(this.i18n('Info'), this.i18n('Upload cancelled')) + } + } + + uploadFirstStep () { + const videofile = this.videofileInput.nativeElement.files[0] as File + if (!videofile) return + + // Cannot upload videos > 8GB for now + if (videofile.size > 8 * 1024 * 1024 * 1024) { + this.notificationsService.error(this.i18n('Error'), this.i18n('We are sorry but PeerTube cannot handle videos > 8GB')) + return + } + + const videoQuota = this.authService.getUser().videoQuota + if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) { + const bytePipes = new BytesPipe() + + const msg = this.i18n( + 'Your video quota is exceeded with this video (video size: {{ videoSize }}, used: {{ videoQuotaUsed }}, quota: {{ videoQuota }})', + { + videoSize: bytePipes.transform(videofile.size, 0), + videoQuotaUsed: bytePipes.transform(this.userVideoQuotaUsed, 0), + videoQuota: bytePipes.transform(videoQuota, 0) + } + ) + this.notificationsService.error(this.i18n('Error'), msg) + return + } + + const nameWithoutExtension = videofile.name.replace(/\.[^/.]+$/, '') + let name: string + + // If the name of the file is very small, keep the extension + if (nameWithoutExtension.length < 3) name = videofile.name + else name = nameWithoutExtension + + const privacy = this.firstStepPrivacyId.toString() + const nsfw = false + const waitTranscoding = true + const commentsEnabled = true + const channelId = this.firstStepChannelId.toString() + + const formData = new FormData() + formData.append('name', name) + // Put the video "private" -> we are waiting the user validation of the second step + formData.append('privacy', VideoPrivacy.PRIVATE.toString()) + formData.append('nsfw', '' + nsfw) + formData.append('commentsEnabled', '' + commentsEnabled) + formData.append('waitTranscoding', '' + waitTranscoding) + formData.append('channelId', '' + channelId) + formData.append('videofile', videofile) + + this.isUploadingVideo = true + this.firstStepDone.emit(name) + + this.form.patchValue({ + name, + privacy, + nsfw, + channelId + }) + + this.videoUploadObservable = this.videoService.uploadVideo(formData).subscribe( + event => { + if (event.type === HttpEventType.UploadProgress) { + this.videoUploadPercents = Math.round(100 * event.loaded / event.total) + } else if (event instanceof HttpResponse) { + this.videoUploaded = true + + this.videoUploadedIds = event.body.video + + this.videoUploadObservable = null + } + }, + + err => { + // Reset progress + this.isUploadingVideo = false + this.videoUploadPercents = 0 + this.videoUploadObservable = null + this.notificationsService.error(this.i18n('Error'), err.message) + } + ) + } + + updateSecondStep () { + if (this.checkForm() === false) { + return + } + + const video = new VideoEdit() + video.patch(this.form.value) + video.id = this.videoUploadedIds.id + video.uuid = this.videoUploadedIds.uuid + + this.isUpdatingVideo = true + this.loadingBar.start() + this.videoService.updateVideo(video) + .pipe( + // Then update captions + switchMap(() => this.videoCaptionService.updateCaptions(video.id, this.videoCaptions)) + ) + .subscribe( + () => { + this.isUpdatingVideo = false + this.isUploadingVideo = false + this.loadingBar.complete() + + this.notificationsService.success(this.i18n('Success'), this.i18n('Video published.')) + this.router.navigate([ '/videos/watch', video.uuid ]) + }, + + err => { + this.isUpdatingVideo = false + this.notificationsService.error(this.i18n('Error'), err.message) + console.error(err) + } + ) + } +} -- cgit v1.2.3