diff --git a/CHANGELOG.md b/CHANGELOG.md index b9535fa94fc1..d119d3d734da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # Changelog +## v6.0.3 + +### IMPORTANT NOTES + + * If you upgrade from PeerTube **< v6.0.0**, please follow v6.0.0 IMPORTANT NOTES + * If you upgrade from PeerTube **v6.0.0**, please follow v6.0.1 IMPORTANT NOTES + +### SECURITY + + * Prevent nginx from serving private/internal/password protected HLS video static files + * You must update your nginx configuration like in [this commit](https://github.com/Chocobozzz/PeerTube/commit/12ea8f0dd11e3fb5fbb8955f5b7d52f27332d619#diff-be9f96b9b1de67284047e610821493f9a5bec86bfcdf81a7d8d6e7904474c186) (line `202` replace `location ~ ^(/static/(webseed|web-videos|streaming-playlists)/private/)|^/download {` by `location ~ ^(/static/(webseed|web-videos|streaming-playlists/hls)/private/)|^/download {`) + +### Bug fixes + + * Fix HTML meta tags with attributes that contain quotes + * Fix time parsing resulting in broken video start time in some cases + * Fix WebTorrent video import crash + * Reload *Discover* page on logout + * Fix privacy error when updating a live, even if the privacy has not changed + * Fix invalid remote live state change notification that causes the player to reload + * Don't apply big play button skin to settings menu + * Fix downloading video files from object storage with some video names (that include emojis, quotes etc) + * Fix thumbnail generation when ffmpeg cannot seek the input + * Fix theme colors on stats page + * Fix input mask (used for chapters, playlist timecodes...) with 10h+ videos + * Fix chapter *position* width consistency + * Fix player ratio with audio only videos + * Also update video playlist URLs when using `update-host` script + * Fix upload/import/update of videos that contain multiple chapters with the same timecode + + ## v6.0.2 ### IMPORTANT NOTES diff --git a/client/angular.json b/client/angular.json index 37af94e99e01..a36c1dac0772 100644 --- a/client/angular.json +++ b/client/angular.json @@ -279,25 +279,25 @@ "serve": { "builder": "@angular-devkit/build-angular:dev-server", "options": { - "browserTarget": "PeerTube:build", - "proxyConfig": "proxy.config.json" + "proxyConfig": "proxy.config.json", + "buildTarget": "PeerTube:build" }, "configurations": { "production": { - "browserTarget": "PeerTube:build:production" + "buildTarget": "PeerTube:build:production" }, "hmr": { - "browserTarget": "PeerTube:build:hmr" + "buildTarget": "PeerTube:build:hmr" }, "ar-locale": { - "browserTarget": "PeerTube:build:ar-locale" + "buildTarget": "PeerTube:build:ar-locale" } } }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { - "browserTarget": "PeerTube:build" + "buildTarget": "PeerTube:build" } }, "lint": { diff --git a/client/package.json b/client/package.json index 8bdddf804ad8..d49df4173f36 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "peertube-client", - "version": "6.0.2", + "version": "6.0.3", "private": true, "license": "AGPL-3.0", "author": { @@ -29,31 +29,31 @@ ], "typings": "*.d.ts", "devDependencies": { - "@angular-devkit/build-angular": "^16.0.2", - "@angular-eslint/builder": "^16.0.2", - "@angular-eslint/eslint-plugin": "^16.0.2", - "@angular-eslint/eslint-plugin-template": "^16.0.2", - "@angular-eslint/schematics": "^16.0.2", - "@angular-eslint/template-parser": "^16.0.2", - "@angular/animations": "^16.0.2", - "@angular/cdk": "^16.0.1", - "@angular/cli": "^16.0.2", - "@angular/common": "^16.0.2", - "@angular/compiler": "^16.0.2", - "@angular/compiler-cli": "^16.0.2", - "@angular/core": "^16.0.2", - "@angular/forms": "^16.0.2", - "@angular/localize": "^16.0.2", - "@angular/platform-browser": "^16.0.2", - "@angular/platform-browser-dynamic": "^16.0.2", - "@angular/router": "^16.0.2", - "@angular/service-worker": "^16.0.2", + "@angular-devkit/build-angular": "^17.0.9", + "@angular-eslint/builder": "^17.1.1", + "@angular-eslint/eslint-plugin": "^17.1.1", + "@angular-eslint/eslint-plugin-template": "^17.1.1", + "@angular-eslint/schematics": "^17.1.1", + "@angular-eslint/template-parser": "^17.1.1", + "@angular/animations": "^17.0.8", + "@angular/cdk": "^17.0.4", + "@angular/cli": "^17.0.9", + "@angular/common": "^17.0.8", + "@angular/compiler": "^17.0.8", + "@angular/compiler-cli": "^17.0.8", + "@angular/core": "^17.0.8", + "@angular/forms": "^17.0.8", + "@angular/localize": "^17.0.8", + "@angular/platform-browser": "^17.0.8", + "@angular/platform-browser-dynamic": "^17.0.8", + "@angular/router": "^17.0.8", + "@angular/service-worker": "^17.0.8", "@babel/core": "^7.18.5", "@babel/preset-env": "^7.18.2", "@formatjs/intl-locale": "^3.3.1", "@formatjs/intl-pluralrules": "^5.2.2", - "@ng-bootstrap/ng-bootstrap": "^15.1.1", - "@ng-select/ng-select": "^11.2.0", + "@ng-bootstrap/ng-bootstrap": "^16.0.0", + "@ng-select/ng-select": "^12.0.4", "@ngx-loading-bar/core": "^6.0.0", "@ngx-loading-bar/http-client": "^6.0.0", "@ngx-loading-bar/router": "^6.0.0", @@ -73,6 +73,7 @@ "@types/lodash-es": "^4.17.0", "@types/markdown-it": "^13.0.2", "@types/node": "^18.13.0", + "@types/qrcode": "^1.5.5", "@types/sanitize-html": "2.9.2", "@types/sha.js": "^2.4.0", "@types/video.js": "^7.3.40", @@ -84,7 +85,7 @@ "@wdio/mocha-framework": "^8.10.4", "@wdio/shared-store-service": "^8.10.5", "@wdio/spec-reporter": "^8.10.5", - "angularx-qrcode": "16.0.0", + "angularx-qrcode": "17.0.0", "babel-loader": "^9.1.0", "bootstrap": "^5.1.3", "buffer": "^6.0.3", @@ -112,7 +113,7 @@ "ngx-uploadx": "^6.1.0", "path-browserify": "^1.0.0", "postcss": "^8.4.14", - "primeng": "^16.0.0-rc.2", + "primeng": "^17.3.1", "raw-loader": "^4.0.2", "rxjs": "^7.3.0", "sanitize-html": "^2.1.2", @@ -126,12 +127,12 @@ "ts-loader": "^9.3.0", "ts-node": "^10.9.1", "tslib": "^2.4.0", - "typescript": "~5.1.0", + "typescript": "~5.2", "video.js": "^7.19.2", "webpack": "^5.73.0", "webpack-bundle-analyzer": "^4.4.2", "webpack-cli": "^5.0.1", - "zone.js": "~0.13.0" + "zone.js": "~0.14.2" }, "dependencies": {} } diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html index a30f01a7ba58..9cd088236f2a 100644 --- a/client/src/app/+accounts/accounts.component.html +++ b/client/src/app/+accounts/accounts.component.html @@ -24,7 +24,7 @@

{{ account.d
- @{{ account.nameWithHost }} + @{{ account.nameWithHost }} VIDEOS

+ + +
+ + + Generate storyboards of local videos using ffmpeg so users can see the video preview in the player while scrubbing the video + + +
+ +
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts index 2c0cc0a165a7..c1cbe1b7d83a 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts @@ -274,6 +274,10 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { instanceCustomHomepage: { content: null + }, + + storyboards: { + enabled: null } } diff --git a/client/src/app/+manage/video-channel-edit/video-channel-edit.component.html b/client/src/app/+manage/video-channel-edit/video-channel-edit.component.html index 8d4cc6f08774..0537f65855e8 100644 --- a/client/src/app/+manage/video-channel-edit/video-channel-edit.component.html +++ b/client/src/app/+manage/video-channel-edit/video-channel-edit.component.html @@ -29,7 +29,7 @@ type="text" id="name" i18n-placeholder placeholder="Example: my_channel" formControlName="name" [ngClass]="{ 'input-error': formErrors['name'] }" class="form-control w-auto flex-grow-1 d-block" > -
@{{ instanceHost }}
+
@{{ instanceHost }}
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html index 154836854014..58a43272e9f2 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html +++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html @@ -2,7 +2,7 @@

My channels - {{ totalItems }} + {{ this.pagination.totalItems }}
@@ -24,7 +24,7 @@

-
No channel found.
+
No channel found.
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 fd63fc22b5e3..56297a31b575 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 @@ -1,6 +1,6 @@ import { ChartData, ChartOptions, TooltipItem, TooltipModel } from 'chart.js' import { max, maxBy, min, minBy } from 'lodash-es' -import { Subject } from 'rxjs' +import { Subject, first, map, switchMap } from 'rxjs' import { Component } from '@angular/core' import { AuthService, ComponentPagination, ConfirmService, hasMoreItems, Notifier, ScreenService } from '@app/core' import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' @@ -11,8 +11,6 @@ import { formatICU } from '@app/helpers' styleUrls: [ './my-video-channels.component.scss' ] }) export class MyVideoChannelsComponent { - totalItems: number - videoChannels: VideoChannel[] = [] videoChannelsChartData: ChartData[] @@ -29,6 +27,8 @@ export class MyVideoChannelsComponent { totalItems: null } + private pagesDone = new Set() + constructor ( private authService: AuthService, private notifier: Notifier, @@ -47,8 +47,7 @@ export class MyVideoChannelsComponent { this.pagination.currentPage = 1 this.videoChannels = [] - this.authService.userInformationLoaded - .subscribe(() => this.loadMoreVideoChannels()) + this.loadMoreVideoChannels() } async deleteVideoChannel (videoChannel: VideoChannel) { @@ -89,19 +88,24 @@ export class MyVideoChannelsComponent { } private loadMoreVideoChannels () { - const user = this.authService.getUser() - const options = { - account: user.account, - withStats: true, - search: this.search, - componentPagination: this.pagination, - sort: '-updatedAt' - } - - return this.videoChannelService.listAccountVideoChannels(options) + if (this.pagesDone.has(this.pagination.currentPage)) return + this.pagesDone.add(this.pagination.currentPage) + + return this.authService.userInformationLoaded + .pipe( + first(), + map(() => ({ + account: this.authService.getUser().account, + withStats: true, + search: this.search, + componentPagination: this.pagination, + sort: '-updatedAt' + })), + switchMap(options => this.videoChannelService.listAccountVideoChannels(options)) + ) .subscribe(res => { this.videoChannels = this.videoChannels.concat(res.data) - this.totalItems = res.total + this.pagination.totalItems = res.total // chart data this.videoChannelsChartData = this.videoChannels.map(v => ({ diff --git a/client/src/app/+my-library/my-videos/my-videos.component.ts b/client/src/app/+my-library/my-videos/my-videos.component.ts index 4a76048783f9..caa3bcb44853 100644 --- a/client/src/app/+my-library/my-videos/my-videos.component.ts +++ b/client/src/app/+my-library/my-videos/my-videos.component.ts @@ -48,7 +48,7 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook { } videoDropdownDisplayOptions: VideoActionsDisplayType = { playlist: false, - download: false, + download: true, update: false, blacklist: false, delete: true, diff --git a/client/src/app/+signup/+register/steps/register-step-channel.component.html b/client/src/app/+signup/+register/steps/register-step-channel.component.html index 342721f3091f..15a9e614af77 100644 --- a/client/src/app/+signup/+register/steps/register-step-channel.component.html +++ b/client/src/app/+signup/+register/steps/register-step-channel.component.html @@ -42,7 +42,7 @@ type="text" id="name" i18n-placeholder placeholder="Example: sweetmelodies24" formControlName="name" [ngClass]="{ 'input-error': formErrors['name'] }" > -
@{{ instanceHost }}
+
@{{ instanceHost }}
diff --git a/client/src/app/+signup/+register/steps/register-step-user.component.html b/client/src/app/+signup/+register/steps/register-step-user.component.html index 1de655bd8bf0..462343e4785e 100644 --- a/client/src/app/+signup/+register/steps/register-step-user.component.html +++ b/client/src/app/+signup/+register/steps/register-step-user.component.html @@ -36,7 +36,7 @@ formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }" autocomplete="username" > - @{{ instanceHost }} + @{{ instanceHost }}
diff --git a/client/src/app/+stats/video/video-stats.component.ts b/client/src/app/+stats/video/video-stats.component.ts index 9217182472cd..2cc5251de489 100644 --- a/client/src/app/+stats/video/video-stats.component.ts +++ b/client/src/app/+stats/video/video-stats.component.ts @@ -1,4 +1,4 @@ -import { ChartConfiguration, ChartData, ChartOptions, PluginOptionsByType, Scale, TooltipItem } from 'chart.js' +import { ChartConfiguration, ChartData, ChartOptions, PluginOptionsByType, Scale, TooltipItem, defaults as ChartJSDefaults } from 'chart.js' import zoomPlugin from 'chartjs-plugin-zoom' import { Observable, of } from 'rxjs' import { SelectOptionsItem } from 'src/types' @@ -18,11 +18,11 @@ import { } from '@peertube/peertube-models' import { VideoStatsService } from './video-stats.service' -type ActiveGraphId = VideoStatsTimeserieMetric | 'retention' | 'countries' +type ActiveGraphId = VideoStatsTimeserieMetric | 'retention' | 'countries' | 'regions' -type CountryData = { name: string, viewers: number }[] +type GeoData = { name: string, viewers: number }[] -type ChartIngestData = VideoStatsTimeserie | VideoStatsRetention | CountryData +type ChartIngestData = VideoStatsTimeserie | VideoStatsRetention | GeoData type ChartBuilderResult = { type: 'line' | 'bar' @@ -35,6 +35,10 @@ type ChartBuilderResult = { type Card = { label: string, value: string | number, moreInfo?: string, help?: string } +ChartJSDefaults.backgroundColor = getComputedStyle(document.body).getPropertyValue('--mainBackgroundColor') +ChartJSDefaults.borderColor = getComputedStyle(document.body).getPropertyValue('--greySecondaryBackgroundColor') +ChartJSDefaults.color = getComputedStyle(document.body).getPropertyValue('--mainForegroundColor') + @Component({ templateUrl: './video-stats.component.html', styleUrls: [ './video-stats.component.scss' ], @@ -55,7 +59,8 @@ export class VideoStatsComponent implements OnInit { video: VideoDetails - countries: CountryData = [] + countries: GeoData = [] + regions: GeoData = [] chartPlugins = [ zoomPlugin ] @@ -100,6 +105,11 @@ export class VideoStatsComponent implements OnInit { id: 'countries', label: $localize`Countries`, zoomEnabled: false + }, + { + id: 'regions', + label: $localize`Regions`, + zoomEnabled: false } ] @@ -136,11 +146,17 @@ export class VideoStatsComponent implements OnInit { return this.countries.length !== 0 } + hasRegions () { + return this.regions.length !== 0 + } + onChartChange (newActive: ActiveGraphId) { this.activeGraphId = newActive if (newActive === 'countries') { this.chartHeight = `${Math.max(this.countries.length * 20, 300)}px` + } else if (newActive === 'regions') { + this.chartHeight = `${Math.max(this.regions.length * 20, 300)}px` } else { this.chartHeight = '300px' } @@ -189,6 +205,8 @@ export class VideoStatsComponent implements OnInit { viewers: c.viewers })) + this.regions = res.subdivisions + this.buildOverallStatCard(res) }, @@ -299,6 +317,13 @@ export class VideoStatsComponent implements OnInit { value: this.numberFormatter.transform(overallStats.countries.length) }) } + + if (overallStats.subdivisions.length !== 0) { + this.overallStatCards.push({ + label: $localize`Regions`, + value: this.numberFormatter.transform(overallStats.subdivisions.length) + }) + } } private loadChart () { @@ -318,7 +343,9 @@ export class VideoStatsComponent implements OnInit { metric: 'viewers' }), - countries: of(this.countries) + countries: of(this.countries), + + regions: of(this.regions) } obsBuilders[this.activeGraphId].subscribe({ @@ -339,7 +366,8 @@ export class VideoStatsComponent implements OnInit { retention: (rawData: VideoStatsRetention) => this.buildRetentionChartOptions(rawData), aggregateWatchTime: (rawData: VideoStatsTimeserie) => this.buildTimeserieChartOptions(rawData), viewers: (rawData: VideoStatsTimeserie) => this.buildTimeserieChartOptions(rawData), - countries: (rawData: CountryData) => this.buildCountryChartOptions(rawData) + countries: (rawData: GeoData) => this.buildGeoChartOptions(rawData), + regions: (rawData: GeoData) => this.buildGeoChartOptions(rawData) } const { type, data, displayLegend, plugins, options } = dataBuilders[graphId](this.chartIngestData[graphId]) @@ -490,7 +518,7 @@ export class VideoStatsComponent implements OnInit { } } - private buildCountryChartOptions (rawData: CountryData): ChartBuilderResult { + private buildGeoChartOptions (rawData: GeoData): ChartBuilderResult { const labels: string[] = [] const data: number[] = [] @@ -570,7 +598,7 @@ export class VideoStatsComponent implements OnInit { if (graphId === 'retention') return value + ' %' if (graphId === 'aggregateWatchTime') return secondsToTime(+value) - if (graphId === 'countries' && scale) return scale.getLabelForValue(value as number) + if ((graphId === 'countries' || graphId === 'regions') && scale) return scale.getLabelForValue(value as number) return value.toLocaleString(this.localeId) } diff --git a/client/src/app/+video-channels/video-channels.component.html b/client/src/app/+video-channels/video-channels.component.html index 228cc4eddaff..572ba4ce971d 100644 --- a/client/src/app/+video-channels/video-channels.component.html +++ b/client/src/app/+video-channels/video-channels.component.html @@ -30,7 +30,7 @@

{{ ownerAccount.displayName }}

-
@{{ videoChannel.ownerBy }}
+
@{{ videoChannel.ownerBy }}
@@ -63,7 +63,7 @@

- @{{ videoChannel.nameWithHost }} + @{{ videoChannel.nameWithHost }}
  • - @channel_id@domain will list the matching channel + @channel_id@domain will list the matching channel
  • URL will list the matching channel diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html index 5f070065ec40..968489d040b2 100644 --- a/client/src/app/menu/menu.component.html +++ b/client/src/app/menu/menu.component.html @@ -13,7 +13,7 @@
    {{ user.account?.displayName }}
    -
    @{{ user.username }}
    +
    @{{ user.username }}