From 7a22a0a56aa75fbb1ba986a5d2c606e1343f30c2 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 5 May 2021 12:10:00 +0200 Subject: Add ability to search live videos --- .../src/app/+search/search-filters.component.html | 27 ++++++++++++--- client/src/app/+search/search-filters.component.ts | 40 +++++++++++++--------- .../shared/shared-search/advanced-search.model.ts | 16 +++++++-- 3 files changed, 61 insertions(+), 22 deletions(-) (limited to 'client/src/app') diff --git a/client/src/app/+search/search-filters.component.html b/client/src/app/+search/search-filters.component.html index 1d1e7b868..421bc7f6f 100644 --- a/client/src/app/+search/search-filters.component.html +++ b/client/src/app/+search/search-filters.component.html @@ -16,6 +16,25 @@ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
@@ -44,7 +63,7 @@
- +
@@ -60,7 +79,7 @@
- +
diff --git a/client/src/app/+search/search-filters.component.ts b/client/src/app/+search/search-filters.component.ts index a2af9a942..59aba22ff 100644 --- a/client/src/app/+search/search-filters.component.ts +++ b/client/src/app/+search/search-filters.component.ts @@ -3,6 +3,8 @@ import { ServerService } from '@app/core' import { AdvancedSearch } from '@app/shared/shared-search' import { ServerConfig, VideoConstant } from '@shared/models' +type FormOption = { id: string, label: string } + @Component({ selector: 'my-search-filters', styleUrls: [ './search-filters.component.scss' ], @@ -17,9 +19,10 @@ export class SearchFiltersComponent implements OnInit { videoLicences: VideoConstant[] = [] videoLanguages: VideoConstant[] = [] - publishedDateRanges: { id: string, label: string }[] = [] - sorts: { id: string, label: string }[] = [] - durationRanges: { id: string, label: string }[] = [] + publishedDateRanges: FormOption[] = [] + sorts: FormOption[] = [] + durationRanges: FormOption[] = [] + videoType: FormOption[] = [] publishedDateRange: string durationRange: string @@ -33,10 +36,6 @@ export class SearchFiltersComponent implements OnInit { private serverService: ServerService ) { this.publishedDateRanges = [ - { - id: 'any_published_date', - label: $localize`Any` - }, { id: 'today', label: $localize`Today` @@ -55,11 +54,18 @@ export class SearchFiltersComponent implements OnInit { } ] - this.durationRanges = [ + this.videoType = [ { - id: 'any_duration', - label: $localize`Any` + id: 'vod', + label: $localize`VOD videos` }, + { + id: 'live', + label: $localize`Live videos` + } + ] + + this.durationRanges = [ { id: 'short', label: $localize`Short (< 4 min)` @@ -104,24 +110,26 @@ export class SearchFiltersComponent implements OnInit { this.loadOriginallyPublishedAtYears() } - inputUpdated () { + onInputUpdated () { this.updateModelFromDurationRange() this.updateModelFromPublishedRange() this.updateModelFromOriginallyPublishedAtYears() } formUpdated () { - this.inputUpdated() + this.onInputUpdated() this.filtered.emit(this.advancedSearch) } reset () { this.advancedSearch.reset() + + this.resetOriginalPublicationYears() + this.durationRange = undefined this.publishedDateRange = undefined - this.originallyPublishedStartYear = undefined - this.originallyPublishedEndYear = undefined - this.inputUpdated() + + this.onInputUpdated() } resetField (fieldName: string, value?: any) { @@ -130,7 +138,7 @@ export class SearchFiltersComponent implements OnInit { resetLocalField (fieldName: string, value?: any) { this[fieldName] = value - this.inputUpdated() + this.onInputUpdated() } resetOriginalPublicationYears () { diff --git a/client/src/app/shared/shared-search/advanced-search.model.ts b/client/src/app/shared/shared-search/advanced-search.model.ts index 0e3924841..2c83f53b6 100644 --- a/client/src/app/shared/shared-search/advanced-search.model.ts +++ b/client/src/app/shared/shared-search/advanced-search.model.ts @@ -1,4 +1,4 @@ -import { BooleanBothQuery, SearchTargetType } from '@shared/models' +import { BooleanBothQuery, BooleanQuery, SearchTargetType, VideosSearchQuery } from '@shared/models' export class AdvancedSearch { startDate: string // ISO 8601 @@ -21,6 +21,8 @@ export class AdvancedSearch { durationMin: number // seconds durationMax: number // seconds + isLive: BooleanQuery + sort: string searchTarget: SearchTargetType @@ -41,6 +43,8 @@ export class AdvancedSearch { tagsOneOf?: any tagsAllOf?: any + isLive?: BooleanQuery + durationMin?: string durationMax?: string sort?: string @@ -54,6 +58,8 @@ export class AdvancedSearch { this.originallyPublishedEndDate = options.originallyPublishedEndDate || undefined this.nsfw = options.nsfw || undefined + this.isLive = options.isLive || undefined + this.categoryOneOf = options.categoryOneOf || undefined this.licenceOneOf = options.licenceOneOf || undefined this.languageOneOf = options.languageOneOf || undefined @@ -94,6 +100,7 @@ export class AdvancedSearch { this.tagsAllOf = undefined this.durationMin = undefined this.durationMax = undefined + this.isLive = undefined this.sort = '-match' } @@ -112,12 +119,16 @@ export class AdvancedSearch { tagsAllOf: this.tagsAllOf, durationMin: this.durationMin, durationMax: this.durationMax, + isLive: this.isLive, sort: this.sort, searchTarget: this.searchTarget } } - toAPIObject () { + toAPIObject (): VideosSearchQuery { + let isLive: boolean + if (this.isLive) isLive = this.isLive === 'true' + return { startDate: this.startDate, endDate: this.endDate, @@ -131,6 +142,7 @@ export class AdvancedSearch { tagsAllOf: this.tagsAllOf, durationMin: this.durationMin, durationMax: this.durationMax, + isLive, sort: this.sort, searchTarget: this.searchTarget } -- cgit v1.2.3 From bc4c9cc1d75d591c217d61ab22a107b7f1044c76 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 7 May 2021 11:25:47 +0200 Subject: Fix mask-image property Needed by chrome --- client/src/app/app.component.scss | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'client/src/app') diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss index e21ada0f1..0543564b4 100644 --- a/client/src/app/app.component.scss +++ b/client/src/app/app.component.scss @@ -40,8 +40,10 @@ } .icon-menu { - background-color: pvar(--mainForegroundColor); mask-image: url('../assets/images/misc/menu.svg'); + -webkit-mask-image: url('../assets/images/misc/menu.svg'); + + background-color: pvar(--mainForegroundColor); margin: 0 18px 0 20px; @media screen and (max-width: $mobile-view) { -- cgit v1.2.3 From ce4b4495ff3607045dc6d5656f72ebf5eb28cb73 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 7 May 2021 14:08:35 +0200 Subject: Refactor video edit css --- .../plugin-search/plugin-search.component.html | 2 +- .../+video-edit/shared/video-edit.component.html | 2 +- .../+video-edit/shared/video-edit.component.scss | 119 ++++++++------------- 3 files changed, 47 insertions(+), 76 deletions(-) (limited to 'client/src/app') diff --git a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html index 6900e8717..8d8f12c48 100644 --- a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html +++ b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html @@ -20,7 +20,7 @@ - {{ pagination.totalItems }} {pagination.totalItems, plural, =1 {result} other {results}} for {{ search }}" + {{ pagination.totalItems }} {pagination.totalItems, plural, =1 {result} other {results}} for "{{ search }}"
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.html b/client/src/app/+videos/+video-edit/shared/video-edit.component.html index 094b4d3b3..16233f9e0 100644 --- a/client/src/app/+videos/+video-edit/shared/video-edit.component.html +++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.html @@ -5,7 +5,7 @@ Basic info -
+
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.scss b/client/src/app/+videos/+video-edit/shared/video-edit.component.scss index bc32d7964..c1c7c686d 100644 --- a/client/src/app/+videos/+video-edit/shared/video-edit.component.scss +++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.scss @@ -1,9 +1,3 @@ -// Bootstrap grid utilities require functions, variables and mixins -@import 'node_modules/bootstrap/scss/functions'; -@import 'node_modules/bootstrap/scss/variables'; -@import 'node_modules/bootstrap/scss/mixins'; -@import 'node_modules/bootstrap/scss/grid'; - @import 'variables'; @import 'mixins'; @@ -57,65 +51,62 @@ my-peertube-checkbox { } } -.captions { - - .captions-header { - text-align: right; - margin-bottom: 1rem; +.captions-header { + text-align: right; + margin-bottom: 1rem; +} - .create-caption { - @include create-button; - } - } +.create-caption { + @include create-button; +} - .caption-entry { - display: flex; - height: 40px; - align-items: center; +.caption-entry { + display: flex; + height: 40px; + align-items: center; - a.caption-entry-label { - @include disable-default-a-behaviour; + a.caption-entry-label { + @include disable-default-a-behaviour; - flex-grow: 1; - color: #000; + flex-grow: 1; + color: #000; - &:hover { - opacity: 0.8; - } + &:hover { + opacity: 0.8; } + } - .caption-entry-label { - font-size: 15px; - font-weight: bold; - - margin-right: 20px; - width: 150px; - } + .caption-entry-label { + font-size: 15px; + font-weight: bold; - .caption-entry-state { - width: 200px; + margin-right: 20px; + width: 150px; + } - &.caption-entry-state-create { - color: #39CC0B; - } + .caption-entry-state { + width: 200px; - &.caption-entry-state-delete { - color: #FF0000; - } + &.caption-entry-state-create { + color: #39CC0B; } - .caption-entry-delete { - @include peertube-button; - @include grey-button; + &.caption-entry-state-delete { + color: #FF0000; } } - .no-caption { - text-align: center; - font-size: 15px; + .caption-entry-delete { + @include peertube-button; + @include grey-button; } } +.no-caption { + text-align: center; + font-size: 15px; +} + .submit-container { text-align: right; @@ -143,35 +134,15 @@ p-calendar { } } -// columns for the video -.col-video-edit { - @include make-col-ready(); +.form-columns { + display: grid; - @include media-breakpoint-up(md) { - @include make-col(7); - - + .col-video-edit { - @include make-col(5); - } - } - - @include media-breakpoint-up(xl) { - @include make-col(8); - - + .col-video-edit { - @include make-col(4); - } - } + grid-template-columns: 66% 1fr; + grid-gap: 30px; } -:host-context(.expanded) { - .col-video-edit { - @include media-breakpoint-up(md) { - @include make-col(8); - - + .col-video-edit { - @include make-col(4); - } - } +@include on-small-main-col { + .form-columns { + grid-template-columns: 1fr; } } -- cgit v1.2.3 From 4076e2ef6b5337cc56caabb8d6cc5f94e60e9347 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 7 May 2021 15:17:43 +0200 Subject: Increase test timeout --- client/src/app/+about/about-follows/about-follows.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'client/src/app') diff --git a/client/src/app/+about/about-follows/about-follows.component.html b/client/src/app/+about/about-follows/about-follows.component.html index f81465f88..6bc1d0448 100644 --- a/client/src/app/+about/about-follows/about-follows.component.html +++ b/client/src/app/+about/about-follows/about-follows.component.html @@ -9,7 +9,7 @@ {{ follower}} - +
-- cgit v1.2.3 From dc2b2938c293bae271a27a6c823f66496998b4d3 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 10 May 2021 09:31:33 +0200 Subject: Sort channels by -updatedAt --- .../account-video-channels.component.ts | 8 +++++++- client/src/app/+accounts/accounts.component.ts | 2 +- .../+my-video-channels/my-video-channels.component.ts | 8 +++++++- .../my-accept-ownership.component.html | 9 ++------- .../my-accept-ownership.component.ts | 16 +++++++--------- .../app/+videos/+video-edit/video-update.resolver.ts | 18 +++++------------- client/src/app/helpers/utils.ts | 18 ++++++++++++------ .../app/shared/shared-main/account/account.model.ts | 6 ++++++ .../src/app/shared/shared-main/account/actor.model.ts | 2 -- .../shared-main/video-channel/video-channel.model.ts | 4 ++++ .../video-channel/video-channel.service.ts | 19 ++++++++++--------- 11 files changed, 61 insertions(+), 49 deletions(-) (limited to 'client/src/app') diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts index 0628c7a96..7e916e122 100644 --- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts +++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts @@ -79,7 +79,13 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy { } loadMoreChannels () { - this.videoChannelService.listAccountVideoChannels(this.account, this.channelPagination) + const options = { + account: this.account, + componentPagination: this.channelPagination, + sort: '-updatedAt' + } + + this.videoChannelService.listAccountVideoChannels(options) .pipe( tap(res => this.channelPagination.totalItems = res.total), switchMap(res => from(res.data)), diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts index fbd7380a9..c69b04a01 100644 --- a/client/src/app/+accounts/accounts.component.ts +++ b/client/src/app/+accounts/accounts.component.ts @@ -66,7 +66,7 @@ export class AccountsComponent implements OnInit, OnDestroy { distinctUntilChanged(), switchMap(accountId => this.accountService.getAccount(accountId)), tap(account => this.onAccount(account)), - switchMap(account => this.videoChannelService.listAccountVideoChannels(account)), + switchMap(account => this.videoChannelService.listAccountVideoChannels({ account })), catchError(err => this.restExtractor.redirectTo404IfNotFound(err, 'other', [ HttpStatusCode.BAD_REQUEST_400, HttpStatusCode.NOT_FOUND_404 diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts index 9e3bf35b4..67b3ee496 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts +++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts @@ -68,8 +68,14 @@ channel with the same name (${videoChannel.name})!`, this.authService.userInformationLoaded .pipe(mergeMap(() => { const user = this.authService.getUser() + const options = { + account: user.account, + withStats: true, + search: this.search, + sort: '-updatedAt' + } - return this.videoChannelService.listAccountVideoChannels(user.account, null, true, this.search) + return this.videoChannelService.listAccountVideoChannels(options) })).subscribe(res => { this.videoChannels = res.data this.totalItems = res.total diff --git a/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html index 088765b20..d0393a2a4 100644 --- a/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html +++ b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.html @@ -8,13 +8,8 @@ diff --git a/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts index 0e2395754..7889d0985 100644 --- a/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts +++ b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts @@ -1,11 +1,12 @@ -import { switchMap } from 'rxjs/operators' +import { SelectChannelItem } from 'src/types/select-options-item.model' import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' import { AuthService, Notifier } from '@app/core' +import { listUserChannels } from '@app/helpers' import { OWNERSHIP_CHANGE_CHANNEL_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators' import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' -import { VideoChannelService, VideoOwnershipService } from '@app/shared/shared-main' +import { VideoOwnershipService } from '@app/shared/shared-main' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' -import { VideoChangeOwnership, VideoChannel } from '@shared/models' +import { VideoChangeOwnership } from '@shared/models' @Component({ selector: 'my-accept-ownership', @@ -18,8 +19,7 @@ export class MyAcceptOwnershipComponent extends FormReactive implements OnInit { @ViewChild('modal', { static: true }) modal: ElementRef videoChangeOwnership: VideoChangeOwnership | undefined = undefined - - videoChannels: VideoChannel[] + videoChannels: SelectChannelItem[] error: string = null @@ -28,7 +28,6 @@ export class MyAcceptOwnershipComponent extends FormReactive implements OnInit { private videoOwnershipService: VideoOwnershipService, private notifier: Notifier, private authService: AuthService, - private videoChannelService: VideoChannelService, private modalService: NgbModal ) { super() @@ -37,9 +36,8 @@ export class MyAcceptOwnershipComponent extends FormReactive implements OnInit { ngOnInit () { this.videoChannels = [] - this.authService.userInformationLoaded - .pipe(switchMap(() => this.videoChannelService.listAccountVideoChannels(this.authService.getUser().account))) - .subscribe(videoChannels => this.videoChannels = videoChannels.data) + listUserChannels(this.authService) + .subscribe(channels => this.videoChannels = channels) this.buildForm({ channel: OWNERSHIP_CHANGE_CHANNEL_VALIDATOR diff --git a/client/src/app/+videos/+video-edit/video-update.resolver.ts b/client/src/app/+videos/+video-edit/video-update.resolver.ts index 276548b79..9172b78a8 100644 --- a/client/src/app/+videos/+video-edit/video-update.resolver.ts +++ b/client/src/app/+videos/+video-edit/video-update.resolver.ts @@ -2,7 +2,9 @@ import { forkJoin, of } from 'rxjs' import { map, switchMap } from 'rxjs/operators' import { Injectable } from '@angular/core' import { ActivatedRouteSnapshot, Resolve } from '@angular/router' -import { VideoCaptionService, VideoChannelService, VideoDetails, VideoService } from '@app/shared/shared-main' +import { AuthService } from '@app/core' +import { listUserChannels } from '@app/helpers' +import { VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main' import { LiveVideoService } from '@app/shared/shared-video-live' @Injectable() @@ -10,7 +12,7 @@ export class VideoUpdateResolver implements Resolve { constructor ( private videoService: VideoService, private liveVideoService: LiveVideoService, - private videoChannelService: VideoChannelService, + private authService: AuthService, private videoCaptionService: VideoCaptionService ) { } @@ -31,17 +33,7 @@ export class VideoUpdateResolver implements Resolve { .loadCompleteDescription(video.descriptionPath) .pipe(map(description => Object.assign(video, { description }))), - this.videoChannelService - .listAccountVideoChannels(video.account) - .pipe( - map(result => result.data), - map(videoChannels => videoChannels.map(c => ({ - id: c.id, - label: c.displayName, - support: c.support, - avatarPath: c.avatar?.path - }))) - ), + listUserChannels(this.authService), this.videoCaptionService .listCaptions(video.id) diff --git a/client/src/app/helpers/utils.ts b/client/src/app/helpers/utils.ts index a1747af3c..17eb5effc 100644 --- a/client/src/app/helpers/utils.ts +++ b/client/src/app/helpers/utils.ts @@ -30,12 +30,18 @@ function listUserChannels (authService: AuthService) { const videoChannels = user.videoChannels if (Array.isArray(videoChannels) === false) return undefined - return videoChannels.map(c => ({ - id: c.id, - label: c.displayName, - support: c.support, - avatarPath: c.avatar?.path - }) as SelectChannelItem) + return videoChannels + .sort((a, b) => { + if (a.updatedAt < b.updatedAt) return 1 + if (a.updatedAt > b.updatedAt) return -1 + return 0 + }) + .map(c => ({ + id: c.id, + label: c.displayName, + support: c.support, + avatarPath: c.avatar?.path + }) as SelectChannelItem) })) } diff --git a/client/src/app/shared/shared-main/account/account.model.ts b/client/src/app/shared/shared-main/account/account.model.ts index 6d9f0ee65..7b5611f35 100644 --- a/client/src/app/shared/shared-main/account/account.model.ts +++ b/client/src/app/shared/shared-main/account/account.model.ts @@ -4,8 +4,12 @@ import { Actor } from './actor.model' export class Account extends Actor implements ServerAccount { displayName: string description: string + + updatedAt: Date | string + nameWithHost: string nameWithHostForced: string + mutedByUser: boolean mutedByInstance: boolean mutedServerByUser: boolean @@ -30,6 +34,8 @@ export class Account extends Actor implements ServerAccount { this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host) this.nameWithHostForced = Actor.CREATE_BY_STRING(this.name, this.host, true) + if (hash.updatedAt) this.updatedAt = new Date(hash.updatedAt.toString()) + this.mutedByUser = false this.mutedByInstance = false this.mutedServerByUser = false diff --git a/client/src/app/shared/shared-main/account/actor.model.ts b/client/src/app/shared/shared-main/account/actor.model.ts index 6ba0bb09e..2fccc472a 100644 --- a/client/src/app/shared/shared-main/account/actor.model.ts +++ b/client/src/app/shared/shared-main/account/actor.model.ts @@ -12,7 +12,6 @@ export abstract class Actor implements ServerActor { followersCount: number createdAt: Date | string - updatedAt: Date | string avatar: ActorImage @@ -55,7 +54,6 @@ export abstract class Actor implements ServerActor { this.followersCount = hash.followersCount if (hash.createdAt) this.createdAt = new Date(hash.createdAt.toString()) - if (hash.updatedAt) this.updatedAt = new Date(hash.updatedAt.toString()) this.avatar = hash.avatar this.isLocal = Actor.IS_LOCAL(this.host) diff --git a/client/src/app/shared/shared-main/video-channel/video-channel.model.ts b/client/src/app/shared/shared-main/video-channel/video-channel.model.ts index c40dd5311..a9dcf2fa2 100644 --- a/client/src/app/shared/shared-main/video-channel/video-channel.model.ts +++ b/client/src/app/shared/shared-main/video-channel/video-channel.model.ts @@ -16,6 +16,8 @@ export class VideoChannel extends Actor implements ServerVideoChannel { banner: ActorImage bannerUrl: string + updatedAt: Date | string + ownerAccount?: ServerAccount ownerBy?: string @@ -59,6 +61,8 @@ export class VideoChannel extends Actor implements ServerVideoChannel { this.videosCount = hash.videosCount + if (hash.updatedAt) this.updatedAt = new Date(hash.updatedAt.toString()) + if (hash.viewsPerDay) { this.viewsPerDay = hash.viewsPerDay.map(v => ({ ...v, date: new Date(v.date) })) } diff --git a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts index e65261763..a89f1065a 100644 --- a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts +++ b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts @@ -40,23 +40,24 @@ export class VideoChannelService { ) } - listAccountVideoChannels ( - account: Account, - componentPagination?: ComponentPaginationLight, - withStats = false, + listAccountVideoChannels (options: { + account: Account + componentPagination?: ComponentPaginationLight + withStats?: boolean + sort?: string search?: string - ): Observable> { + }): Observable> { + const { account, componentPagination, withStats = false, sort, search } = options + const pagination = componentPagination ? this.restService.componentPaginationToRestPagination(componentPagination) : { start: 0, count: 20 } let params = new HttpParams() - params = this.restService.addRestGetParams(params, pagination) + params = this.restService.addRestGetParams(params, pagination, sort) params = params.set('withStats', withStats + '') - if (search) { - params = params.set('search', search) - } + if (search) params = params.set('search', search) const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-channels' return this.authHttp.get>(url, { params }) -- cgit v1.2.3 From f6d6e7f861189a4446f406efb775a29688764b48 Mon Sep 17 00:00:00 2001 From: kontrollanten <6680299+kontrollanten@users.noreply.github.com> Date: Mon, 10 May 2021 11:13:41 +0200 Subject: Resumable video uploads (#3933) * WIP: resumable video uploads relates to #324 * fix review comments * video upload: error handling * fix audio upload * fixes after self review * Update server/controllers/api/videos/index.ts Co-authored-by: Rigel Kent * Update server/middlewares/validators/videos/videos.ts Co-authored-by: Rigel Kent * Update server/controllers/api/videos/index.ts Co-authored-by: Rigel Kent * update after code review * refactor upload route - restore multipart upload route - move resumable to dedicated upload-resumable route - move checks to middleware - do not leak internal fs structure in response * fix yarn.lock upon rebase * factorize addVideo for reuse in both endpoints * add resumable upload API to openapi spec * add initial test and test helper for resumable upload * typings for videoAddResumable middleware * avoid including aws and google packages via node-uploadx, by only including uploadx/core * rename ex-isAudioBg to more explicit name mentioning it is a preview file for audio * add video-upload-tmp-folder-cleaner job * stronger typing of video upload middleware * reduce dependency to @uploadx/core * add audio upload test * refactor resumable uploads cleanup from job to scheduler * refactor resumable uploads scheduler to compare to last execution time * make resumable upload validator to always cleanup on failure * move legacy upload request building outside of uploadVideo test helper * filter upload-resumable middlewares down to POST, PUT, DELETE also begin to type metadata * merge add duration functions * stronger typings and documentation for uploadx behaviour, move init validator up * refactor(client/video-edit): options > uploadxOptions * refactor(client/video-edit): remove obsolete else * scheduler/remove-dangling-resum: rename tag * refactor(server/video): add UploadVideoFiles type * refactor(mw/validators): restructure eslint disable * refactor(mw/validators/videos): rename import * refactor(client/vid-upload): rename html elem id * refactor(sched/remove-dangl): move fn to method * refactor(mw/async): add method typing * refactor(mw/vali/video): double quote > single * refactor(server/upload-resum): express use > all * proper http methud enum server/middlewares/async.ts * properly type http methods * factorize common video upload validation steps * add check for maximum partially uploaded file size * fix audioBg use * fix extname(filename) in addVideo * document parameters for uploadx's resumable protocol * clear META files in scheduler * last audio refactor before cramming preview in the initial POST form data * refactor as mulitpart/form-data initial post request this allows preview/thumbnail uploads alongside the initial request, and cleans up the upload form * Add more tests for resumable uploads * Refactor remove dangling resumable uploads * Prepare changelog * Add more resumable upload tests * Remove user quota check for resumable uploads * Fix upload error handler * Update nginx template for upload-resumable * Cleanup comment * Remove unused express methods * Prefer to use got instead of raw http * Don't retry on error 500 Co-authored-by: Rigel Kent Co-authored-by: Rigel Kent Co-authored-by: Chocobozzz --- .../my-account-settings.component.ts | 4 +- .../my-video-channel-update.component.ts | 6 +- .../video-add-components/uploaderx-form-data.ts | 48 ++++ .../video-upload.component.html | 20 +- .../video-upload.component.scss | 4 - .../video-add-components/video-upload.component.ts | 295 ++++++++++++--------- .../app/+videos/+video-edit/video-add.module.ts | 5 +- client/src/app/helpers/utils.ts | 9 +- 8 files changed, 243 insertions(+), 148 deletions(-) create mode 100644 client/src/app/+videos/+video-edit/video-add-components/uploaderx-form-data.ts (limited to 'client/src/app') diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts index c16368952..a0f2f28f8 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts @@ -2,7 +2,7 @@ import { ViewportScroller } from '@angular/common' import { HttpErrorResponse } from '@angular/common/http' import { AfterViewChecked, Component, OnInit } from '@angular/core' import { AuthService, Notifier, User, UserService } from '@app/core' -import { uploadErrorHandler } from '@app/helpers' +import { genericUploadErrorHandler } from '@app/helpers' @Component({ selector: 'my-account-settings', @@ -46,7 +46,7 @@ export class MyAccountSettingsComponent implements OnInit, AfterViewChecked { this.user.updateAccountAvatar(data.avatar) }, - (err: HttpErrorResponse) => uploadErrorHandler({ + (err: HttpErrorResponse) => genericUploadErrorHandler({ err, name: $localize`avatar`, notifier: this.notifier diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts index a29af176c..c9173039a 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts +++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts @@ -3,7 +3,7 @@ import { HttpErrorResponse } from '@angular/common/http' import { Component, OnDestroy, OnInit } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' import { AuthService, Notifier, ServerService } from '@app/core' -import { uploadErrorHandler } from '@app/helpers' +import { genericUploadErrorHandler } from '@app/helpers' import { VIDEO_CHANNEL_DESCRIPTION_VALIDATOR, VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, @@ -109,7 +109,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements this.videoChannel.updateAvatar(data.avatar) }, - (err: HttpErrorResponse) => uploadErrorHandler({ + (err: HttpErrorResponse) => genericUploadErrorHandler({ err, name: $localize`avatar`, notifier: this.notifier @@ -139,7 +139,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements this.videoChannel.updateBanner(data.banner) }, - (err: HttpErrorResponse) => uploadErrorHandler({ + (err: HttpErrorResponse) => genericUploadErrorHandler({ err, name: $localize`banner`, notifier: this.notifier diff --git a/client/src/app/+videos/+video-edit/video-add-components/uploaderx-form-data.ts b/client/src/app/+videos/+video-edit/video-add-components/uploaderx-form-data.ts new file mode 100644 index 000000000..3392a0d8a --- /dev/null +++ b/client/src/app/+videos/+video-edit/video-add-components/uploaderx-form-data.ts @@ -0,0 +1,48 @@ +import { objectToFormData } from '@app/helpers' +import { resolveUrl, UploaderX } from 'ngx-uploadx' + +/** + * multipart/form-data uploader extending the UploaderX implementation of Google Resumable + * for use with multer + * + * @see https://github.com/kukhariev/ngx-uploadx/blob/637e258fe366b8095203f387a6101a230ee4f8e6/src/uploadx/lib/uploaderx.ts + * @example + * + * options: UploadxOptions = { + * uploaderClass: UploaderXFormData + * }; + */ +export class UploaderXFormData extends UploaderX { + + async getFileUrl (): Promise { + const headers = { + 'X-Upload-Content-Length': this.size.toString(), + 'X-Upload-Content-Type': this.file.type || 'application/octet-stream' + } + + const previewfile = this.metadata.previewfile as any as File + delete this.metadata.previewfile + + const data = objectToFormData(this.metadata) + if (previewfile !== undefined) { + data.append('previewfile', previewfile, previewfile.name) + data.append('thumbnailfile', previewfile, previewfile.name) + } + + await this.request({ + method: 'POST', + body: data, + url: this.endpoint, + headers + }) + + const location = this.getValueFromResponse('location') + if (!location) { + throw new Error('Invalid or missing Location header') + } + + this.offset = this.responseStatus === 201 ? 0 : undefined + + return resolveUrl(location, this.endpoint) + } +} diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html index 4c0b09894..86a779f8a 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html +++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html @@ -1,12 +1,17 @@ -
+
Select the file to upload
@@ -41,7 +46,13 @@
- + +
@@ -64,6 +75,7 @@ {{ error }}
+
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss index 9549257f6..d9f348a70 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss +++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss @@ -47,8 +47,4 @@ margin-left: 10px; } - - .btn-group > input:not(:first-child) { - margin-left: 0; - } } diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts index effb37077..2d3fc3578 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts +++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts @@ -1,15 +1,16 @@ -import { Subscription } from 'rxjs' -import { HttpErrorResponse, HttpEventType, HttpResponse } from '@angular/common/http' import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' import { Router } from '@angular/router' +import { UploadxOptions, UploadState, UploadxService } from 'ngx-uploadx' +import { UploaderXFormData } from './uploaderx-form-data' import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService, UserService } from '@app/core' -import { scrollToTop, uploadErrorHandler } from '@app/helpers' +import { scrollToTop, genericUploadErrorHandler } from '@app/helpers' import { FormValidatorService } from '@app/shared/shared-forms' import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' import { LoadingBarService } from '@ngx-loading-bar/core' import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' import { VideoPrivacy } from '@shared/models' import { VideoSend } from './video-send' +import { HttpErrorResponse, HttpEventType, HttpHeaders } from '@angular/common/http' @Component({ selector: 'my-video-upload', @@ -20,23 +21,18 @@ import { VideoSend } from './video-send' './video-send.scss' ] }) -export class VideoUploadComponent extends VideoSend implements OnInit, AfterViewInit, OnDestroy, CanComponentDeactivate { +export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, AfterViewInit, CanComponentDeactivate { @Output() firstStepDone = new EventEmitter() @Output() firstStepError = new EventEmitter() @ViewChild('videofileInput') videofileInput: ElementRef - // So that it can be accessed in the template - readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY - userVideoQuotaUsed = 0 userVideoQuotaUsedDaily = 0 isUploadingAudioFile = false isUploadingVideo = false - isUpdatingVideo = false videoUploaded = false - videoUploadObservable: Subscription = null videoUploadPercents = 0 videoUploadedIds = { id: 0, @@ -49,7 +45,13 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView error: string enableRetryAfterError: boolean + // So that it can be accessed in the template protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC + protected readonly BASE_VIDEO_UPLOAD_URL = VideoService.BASE_VIDEO_URL + 'upload-resumable' + + private uploadxOptions: UploadxOptions + private isUpdatingVideo = false + private fileToUpload: File constructor ( protected formValidatorService: FormValidatorService, @@ -61,15 +63,77 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView protected videoCaptionService: VideoCaptionService, private userService: UserService, private router: Router, - private hooks: HooksService - ) { + private hooks: HooksService, + private resumableUploadService: UploadxService + ) { super() + + this.uploadxOptions = { + endpoint: this.BASE_VIDEO_UPLOAD_URL, + multiple: false, + token: this.authService.getAccessToken(), + uploaderClass: UploaderXFormData, + retryConfig: { + maxAttempts: 6, + shouldRetry: (code: number) => { + return code < 400 || code >= 501 + } + } + } } get videoExtensions () { return this.serverConfig.video.file.extensions.join(', ') } + onUploadVideoOngoing (state: UploadState) { + switch (state.status) { + case 'error': + const error = state.response?.error || 'Unknow error' + + this.handleUploadError({ + error: new Error(error), + name: 'HttpErrorResponse', + message: error, + ok: false, + headers: new HttpHeaders(state.responseHeaders), + status: +state.responseStatus, + statusText: error, + type: HttpEventType.Response, + url: state.url + }) + break + + case 'cancelled': + this.isUploadingVideo = false + this.videoUploadPercents = 0 + + this.firstStepError.emit() + this.enableRetryAfterError = false + this.error = '' + break + + case 'queue': + this.closeFirstStep(state.name) + break + + case 'uploading': + this.videoUploadPercents = state.progress + break + + case 'paused': + this.notifier.info($localize`Upload cancelled`) + break + + case 'complete': + this.videoUploaded = true + this.videoUploadPercents = 100 + + this.videoUploadedIds = state?.response.video + break + } + } + ngOnInit () { super.ngOnInit() @@ -78,6 +142,9 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView this.userVideoQuotaUsed = data.videoQuotaUsed this.userVideoQuotaUsedDaily = data.videoQuotaUsedDaily }) + + this.resumableUploadService.events + .subscribe(state => this.onUploadVideoOngoing(state)) } ngAfterViewInit () { @@ -85,7 +152,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView } ngOnDestroy () { - if (this.videoUploadObservable) this.videoUploadObservable.unsubscribe() + this.cancelUpload() } canDeactivate () { @@ -105,137 +172,43 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView } } - getVideoFile () { - return this.videofileInput.nativeElement.files[0] - } - - setVideoFile (files: FileList) { + onFileDropped (files: FileList) { this.videofileInput.nativeElement.files = files - this.fileChange() - } - - getAudioUploadLabel () { - const videofile = this.getVideoFile() - if (!videofile) return $localize`Upload` - return $localize`Upload ${videofile.name}` + this.onFileChange({ target: this.videofileInput.nativeElement }) } - fileChange () { - this.uploadFirstStep() - } - - retryUpload () { - this.enableRetryAfterError = false - this.error = '' - this.uploadVideo() - } - - cancelUpload () { - if (this.videoUploadObservable !== null) { - this.videoUploadObservable.unsubscribe() - } - - this.isUploadingVideo = false - this.videoUploadPercents = 0 - this.videoUploadObservable = null + onFileChange (event: Event | { target: HTMLInputElement }) { + const file = (event.target as HTMLInputElement).files[0] - this.firstStepError.emit() - this.enableRetryAfterError = false - this.error = '' + if (!file) return - this.notifier.info($localize`Upload cancelled`) - } + if (!this.checkGlobalUserQuota(file)) return + if (!this.checkDailyUserQuota(file)) return - uploadFirstStep (clickedOnButton = false) { - const videofile = this.getVideoFile() - if (!videofile) return - - if (!this.checkGlobalUserQuota(videofile)) return - if (!this.checkDailyUserQuota(videofile)) return - - if (clickedOnButton === false && this.isAudioFile(videofile.name)) { + if (this.isAudioFile(file.name)) { this.isUploadingAudioFile = true return } - // Build name field - 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 nsfw = this.serverConfig.instance.isNSFW - const waitTranscoding = true - const commentsEnabled = true - const downloadEnabled = true - const channelId = this.firstStepChannelId.toString() - - this.formData = new FormData() - this.formData.append('name', name) - // Put the video "private" -> we are waiting the user validation of the second step - this.formData.append('privacy', VideoPrivacy.PRIVATE.toString()) - this.formData.append('nsfw', '' + nsfw) - this.formData.append('commentsEnabled', '' + commentsEnabled) - this.formData.append('downloadEnabled', '' + downloadEnabled) - this.formData.append('waitTranscoding', '' + waitTranscoding) - this.formData.append('channelId', '' + channelId) - this.formData.append('videofile', videofile) - - if (this.previewfileUpload) { - this.formData.append('previewfile', this.previewfileUpload) - this.formData.append('thumbnailfile', this.previewfileUpload) - } - this.isUploadingVideo = true - this.firstStepDone.emit(name) - - this.form.patchValue({ - name, - privacy: this.firstStepPrivacyId, - nsfw, - channelId: this.firstStepChannelId, - previewfile: this.previewfileUpload - }) + this.fileToUpload = file - this.uploadVideo() + this.uploadFile(file) } - uploadVideo () { - this.videoUploadObservable = this.videoService.uploadVideo(this.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 - } - }, + uploadAudio () { + this.uploadFile(this.getInputVideoFile(), this.previewfileUpload) + } - (err: HttpErrorResponse) => { - // Reset progress (but keep isUploadingVideo true) - this.videoUploadPercents = 0 - this.videoUploadObservable = null - this.enableRetryAfterError = true - - this.error = uploadErrorHandler({ - err, - name: $localize`video`, - notifier: this.notifier, - sticky: false - }) + retryUpload () { + this.enableRetryAfterError = false + this.error = '' + this.uploadFile(this.fileToUpload) + } - if (err.status === HttpStatusCode.PAYLOAD_TOO_LARGE_413 || - err.status === HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415) { - this.cancelUpload() - } - } - ) + cancelUpload () { + this.resumableUploadService.control({ action: 'cancel' }) } isPublishingButtonDisabled () { @@ -245,6 +218,13 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView !this.videoUploadedIds.id } + getAudioUploadLabel () { + const videofile = this.getInputVideoFile() + if (!videofile) return $localize`Upload` + + return $localize`Upload ${videofile.name}` + } + updateSecondStep () { if (this.isPublishingButtonDisabled() || !this.checkForm()) { return @@ -275,6 +255,62 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView ) } + private getInputVideoFile () { + return this.videofileInput.nativeElement.files[0] + } + + private uploadFile (file: File, previewfile?: File) { + const metadata = { + waitTranscoding: true, + commentsEnabled: true, + downloadEnabled: true, + channelId: this.firstStepChannelId, + nsfw: this.serverConfig.instance.isNSFW, + privacy: VideoPrivacy.PRIVATE.toString(), + filename: file.name, + previewfile: previewfile as any + } + + this.resumableUploadService.handleFiles(file, { + ...this.uploadxOptions, + metadata + }) + + this.isUploadingVideo = true + } + + private handleUploadError (err: HttpErrorResponse) { + // Reset progress (but keep isUploadingVideo true) + this.videoUploadPercents = 0 + this.enableRetryAfterError = true + + this.error = genericUploadErrorHandler({ + err, + name: $localize`video`, + notifier: this.notifier, + sticky: false + }) + + if (err.status === HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415) { + this.cancelUpload() + } + } + + private closeFirstStep (filename: string) { + const nameWithoutExtension = filename.replace(/\.[^/.]+$/, '') + const name = nameWithoutExtension.length < 3 ? filename : nameWithoutExtension + + this.form.patchValue({ + name, + privacy: this.firstStepPrivacyId, + nsfw: this.serverConfig.instance.isNSFW, + channelId: this.firstStepChannelId, + previewfile: this.previewfileUpload + }) + + this.firstStepDone.emit(name) + } + private checkGlobalUserQuota (videofile: File) { const bytePipes = new BytesPipe() @@ -285,8 +321,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, AfterView const videoQuotaUsedBytes = bytePipes.transform(this.userVideoQuotaUsed, 0) const videoQuotaBytes = bytePipes.transform(videoQuota, 0) - const msg = $localize`Your video quota is exceeded with this video ( -video size: ${videoSizeBytes}, used: ${videoQuotaUsedBytes}, quota: ${videoQuotaBytes})` + const msg = $localize`Your video quota is exceeded with this video (video size: ${videoSizeBytes}, used: ${videoQuotaUsedBytes}, quota: ${videoQuotaBytes})` this.notifier.error(msg) return false @@ -304,9 +339,7 @@ video size: ${videoSizeBytes}, used: ${videoQuotaUsedBytes}, quota: ${videoQuota const videoSizeBytes = bytePipes.transform(videofile.size, 0) const quotaUsedDailyBytes = bytePipes.transform(this.userVideoQuotaUsedDaily, 0) const quotaDailyBytes = bytePipes.transform(videoQuotaDaily, 0) - - const msg = $localize`Your daily video quota is exceeded with this video ( -video size: ${videoSizeBytes}, used: ${quotaUsedDailyBytes}, quota: ${quotaDailyBytes})` + 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 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 da651119b..e836cf81e 100644 --- a/client/src/app/+videos/+video-edit/video-add.module.ts +++ b/client/src/app/+videos/+video-edit/video-add.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core' import { CanDeactivateGuard } from '@app/core' +import { UploadxModule } from 'ngx-uploadx' import { VideoEditModule } from './shared/video-edit.module' import { DragDropDirective } from './video-add-components/drag-drop.directive' import { VideoImportTorrentComponent } from './video-add-components/video-import-torrent.component' @@ -13,7 +14,9 @@ import { VideoAddComponent } from './video-add.component' imports: [ VideoAddRoutingModule, - VideoEditModule + VideoEditModule, + + UploadxModule ], declarations: [ diff --git a/client/src/app/helpers/utils.ts b/client/src/app/helpers/utils.ts index 17eb5effc..d6ac5b9b4 100644 --- a/client/src/app/helpers/utils.ts +++ b/client/src/app/helpers/utils.ts @@ -173,8 +173,8 @@ function isXPercentInViewport (el: HTMLElement, percentVisible: number) { ) } -function uploadErrorHandler (parameters: { - err: HttpErrorResponse +function genericUploadErrorHandler (parameters: { + err: Pick name: string notifier: Notifier sticky?: boolean @@ -186,6 +186,9 @@ function uploadErrorHandler (parameters: { if (err instanceof ErrorEvent) { // network error message = $localize`The connection was interrupted` notifier.error(message, title, null, sticky) + } else if (err.status === HttpStatusCode.INTERNAL_SERVER_ERROR_500) { + message = $localize`The server encountered an error` + notifier.error(message, title, null, sticky) } else if (err.status === HttpStatusCode.REQUEST_TIMEOUT_408) { message = $localize`Your ${name} file couldn't be transferred before the set timeout (usually 10min)` notifier.error(message, title, null, sticky) @@ -216,5 +219,5 @@ export { isInViewport, isXPercentInViewport, listUserChannels, - uploadErrorHandler + genericUploadErrorHandler } -- cgit v1.2.3 From 66615377644fc71a0f38e1b1edc31865e7d7427a Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 10 May 2021 15:16:53 +0200 Subject: Fix miniature overflow --- .../app/shared/shared-video-miniature/video-miniature.component.scss | 2 ++ 1 file changed, 2 insertions(+) (limited to 'client/src/app') diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss index 5df89d019..0bbdff1e6 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss @@ -95,6 +95,7 @@ my-actor-avatar { .video-bottom { display: flex; width: 100%; + min-width: 1px; } .video-miniature-name { @@ -145,6 +146,7 @@ my-actor-avatar { .video-bottom { display: flex; + min-width: 1px; } // We don't display avatar in row mode -- cgit v1.2.3 From ff0497fee85db13e81c90224d75e491faea0d0e3 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 10 May 2021 15:31:18 +0200 Subject: Fix button icon in admin plugins --- client/src/app/shared/shared-main/buttons/button.component.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'client/src/app') diff --git a/client/src/app/shared/shared-main/buttons/button.component.scss b/client/src/app/shared/shared-main/buttons/button.component.scss index 09b5f95d7..22b24c853 100644 --- a/client/src/app/shared/shared-main/buttons/button.component.scss +++ b/client/src/app/shared/shared-main/buttons/button.component.scss @@ -30,7 +30,7 @@ span[class$=-button] { .action-button { @include peertube-button-link; - @include button-with-icon(21px, 0, -1px); + @include button-with-icon(21px); } .orange-button { -- cgit v1.2.3 From 1fa23d6f5e487f3c149e1f0001beadd919ee82fc Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 10 May 2021 15:48:10 +0200 Subject: Fix video update --- client/src/app/helpers/utils.ts | 45 ++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 21 deletions(-) (limited to 'client/src/app') diff --git a/client/src/app/helpers/utils.ts b/client/src/app/helpers/utils.ts index d6ac5b9b4..94f6def26 100644 --- a/client/src/app/helpers/utils.ts +++ b/client/src/app/helpers/utils.ts @@ -1,4 +1,4 @@ -import { map } from 'rxjs/operators' +import { first, map } from 'rxjs/operators' import { SelectChannelItem } from 'src/types/select-options-item.model' import { DatePipe } from '@angular/common' import { HttpErrorResponse } from '@angular/common/http' @@ -23,26 +23,29 @@ function getParameterByName (name: string, url: string) { function listUserChannels (authService: AuthService) { return authService.userInformationLoaded - .pipe(map(() => { - const user = authService.getUser() - if (!user) return undefined - - const videoChannels = user.videoChannels - if (Array.isArray(videoChannels) === false) return undefined - - return videoChannels - .sort((a, b) => { - if (a.updatedAt < b.updatedAt) return 1 - if (a.updatedAt > b.updatedAt) return -1 - return 0 - }) - .map(c => ({ - id: c.id, - label: c.displayName, - support: c.support, - avatarPath: c.avatar?.path - }) as SelectChannelItem) - })) + .pipe( + first(), + map(() => { + const user = authService.getUser() + if (!user) return undefined + + const videoChannels = user.videoChannels + if (Array.isArray(videoChannels) === false) return undefined + + return videoChannels + .sort((a, b) => { + if (a.updatedAt < b.updatedAt) return 1 + if (a.updatedAt > b.updatedAt) return -1 + return 0 + }) + .map(c => ({ + id: c.id, + label: c.displayName, + support: c.support, + avatarPath: c.avatar?.path + }) as SelectChannelItem) + }) + ) } function getAbsoluteAPIUrl () { -- cgit v1.2.3 From 71fb8b5a344a91ecf9ecc98b6acebeb96d41b834 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 11 May 2021 08:41:26 +0200 Subject: Fix paused upload message --- .../+videos/+video-edit/video-add-components/video-upload.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'client/src/app') diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts index 2d3fc3578..bca1b6eb6 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts +++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts @@ -122,7 +122,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy break case 'paused': - this.notifier.info($localize`Upload cancelled`) + this.notifier.info($localize`Upload on hold`) break case 'complete': -- cgit v1.2.3 From 428ccb8b7a44ce60cabb7401a5464cf5fcbd4dba Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 11 May 2021 12:04:47 +0200 Subject: Reorganize plugin models --- .../plugin-list-installed/plugin-list-installed.component.ts | 3 +-- .../+admin/plugins/plugin-search/plugin-search.component.ts | 3 +-- .../app/+videos/+video-edit/shared/video-edit.component.ts | 11 +++++++++-- 3 files changed, 11 insertions(+), 6 deletions(-) (limited to 'client/src/app') diff --git a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts index 1a95980ae..6af224920 100644 --- a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts +++ b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts @@ -5,8 +5,7 @@ import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' import { ComponentPagination, ConfirmService, hasMoreItems, Notifier } from '@app/core' import { PluginService } from '@app/core/plugins/plugin.service' import { compareSemVer } from '@shared/core-utils/miscs/miscs' -import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model' -import { PluginType } from '@shared/models/plugins/plugin.type' +import { PeerTubePlugin, PluginType } from '@shared/models' @Component({ selector: 'my-plugin-list-installed', diff --git a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts index d2c179aba..0a6e57904 100644 --- a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts +++ b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts @@ -4,8 +4,7 @@ import { Component, OnInit } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' import { ComponentPagination, ConfirmService, hasMoreItems, Notifier, PluginService } from '@app/core' -import { PeerTubePluginIndex } from '@shared/models/plugins/peertube-plugin-index.model' -import { PluginType } from '@shared/models/plugins/plugin.type' +import { PeerTubePluginIndex, PluginType } from '@shared/models' @Component({ selector: 'my-plugin-search', diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts index 34119f7ab..3d916dbce 100644 --- a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts +++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts @@ -21,8 +21,15 @@ import { import { FormReactiveValidationMessages, FormValidatorService } from '@app/shared/shared-forms' import { InstanceService } from '@app/shared/shared-instance' import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main' -import { LiveVideo, ServerConfig, VideoConstant, VideoDetails, VideoPrivacy } from '@shared/models' -import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model' +import { + LiveVideo, + RegisterClientFormFieldOptions, + RegisterClientVideoFieldOptions, + ServerConfig, + VideoConstant, + VideoDetails, + VideoPrivacy +} from '@shared/models' import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service' import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' import { VideoEditType } from './video-edit.type' -- cgit v1.2.3 From 32985a0a779e0e2100a3990b1f1188645e58dfb1 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 11 May 2021 14:56:00 +0200 Subject: Error if importing a torrent with multiple files --- .../video-add-components/video-import-torrent.component.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'client/src/app') diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts index 3aae24732..23bd5ef76 100644 --- a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts +++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts @@ -5,7 +5,7 @@ import { scrollToTop } from '@app/helpers' import { FormValidatorService } from '@app/shared/shared-forms' import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' import { LoadingBarService } from '@ngx-loading-bar/core' -import { VideoPrivacy, VideoUpdate } from '@shared/models' +import { ServerErrorCode, VideoPrivacy, VideoUpdate } from '@shared/models' import { hydrateFormFromVideo } from '../shared/video-edit-utils' import { VideoSend } from './video-send' @@ -113,7 +113,13 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Af this.loadingBar.useRef().complete() this.isImportingVideo = false this.firstStepError.emit() - this.notifier.error(err.message) + + let message = err.message + if (err.body?.code === ServerErrorCode.INCORRECT_FILES_IN_TORRENT) { + message = $localize`Torrents with only 1 file are supported.` + } + + this.notifier.error(message) } ) } -- cgit v1.2.3 From 3914a50b0732076707a6e4e22f54254ae00fc829 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 11 May 2021 15:03:30 +0200 Subject: Remember tab in upload page --- .../app/+videos/+video-edit/video-add.component.html | 10 +++++----- .../app/+videos/+video-edit/video-add.component.ts | 20 ++++++++++++++++++-- 2 files changed, 23 insertions(+), 7 deletions(-) (limited to 'client/src/app') 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 dc8c2f21d..ac75d9ff8 100644 --- a/client/src/app/+videos/+video-edit/video-add.component.html +++ b/client/src/app/+videos/+video-edit/video-add.component.html @@ -20,8 +20,8 @@ Upload {{ videoName }}
-