1
0
Fork 0
-----BEGIN PGP SIGNATURE-----
 
 iQEzBAABCAAdFiEExEqtY4NnkSypPt1XWDphLYkBWb4FAmV2w9QACgkQWDphLYkB
 Wb4fbQf/QSMa7zHYkGzgu9mxmyWjCbXzTLQQAWIe+11w9uWURoRKSAAbQ09a+8zy
 TC2F9zhJhbo9zGbgo+nBbn6hxWSPgtTL9CDJvRR+ObMyGe8TXImDaZoqUI+UFUEy
 jv3eeeA27piBir8NV0cKEcTReIzSY/y6sQYOGg++Cx29nWPb4ce/6HP/WB4BR80c
 8u9hQc9nF/fkNnmaVCxpqCkEfzCoCjXfQSctjEhTrgAJ38aBYuJ4Vc4+i3oYVnuU
 gO8BjFTuONUFQF04aXmClSQj8dGSugtwNJOnvpUNu9isWJSZLQ22erhcnBsJkgEM
 o2k0msMr1Jxc6ngPoqPIy27WqXiXKw==
 =XVQf
 -----END PGP SIGNATURE-----
gpgsig -----BEGIN PGP SIGNATURE-----
 
 iQJNBAABCAA3FiEEGCBYi9NGimfngx9dVTwOu+tdXwgFAmV6S0oZHGtvdG92YWxl
 eGFyaWFuQGdtYWlsLmNvbQAKCRBVPA67611fCHweEADP4ElWrWMrH4kjukiMUn83
 rtuDCv50tCGDrgfq73fPyMusMD54reEwBtSfmIfiPhSqanuoDrX4gtrJCfMZ2MLe
 YAPyS1E4Ma1rbd8qSljeW8pWCdOmJ35rhj+jv8WCUpo41UsMxtApyhNa218kuYZU
 dfywVfdJY+RgU+FtlVnR2KuMc3ASCTFrHqPxe8Kvy+g9Mip5o9sP+FKL2zXUMizz
 Wln7urCIkF52j49qPDv8NPLobWBxYmyidSoZDVl1Ydl6djCVKNDi5U5T04ojp/7V
 a2wFK+GhinNRotVPdVX1HkdkVSBXMFFG14wj6n/n6mvOsWX51Aj1yoFdkmvHdGdK
 62/F6kThQf7XdKzlehr0UUHvgA/HyKSS+V5V2wCIjPhUVf7Kp5IXEO8cibEcEpVh
 FGkrwd+MdycHpUIQPWPrOP6mrugJdDw7Ir9u5nxGTlB6Y0PXua0xPnxfP5+uLVjE
 6DdHUUFbd0rPPiVcqqNuRNSwuazA/UT4APq1CHA74ME3qP/eI09ePaUvyXVXUgJo
 nQTsp8g7BhlptUhMmeuW1XGkLfaTYVMAbMpiGz4S/wxnqIi2R75CE70BV06ZBFOF
 DWAEJR+i1138h7rqppQPW2cqpDTC5RWnkfkC9mJEJOIKS+pzZ/iU7fpjWAL67WrM
 11DvAKOcMQRTbw5ROLkDIA==
 =THDy
 -----END PGP SIGNATURE-----

Merge tag 'v6.0.2' into changes

v6.0.2
This commit is contained in:
Alex Kotov 2023-12-14 04:24:40 +04:00
commit 3438b07487
30 changed files with 187 additions and 60 deletions

View File

@ -1,5 +1,26 @@
# Changelog # Changelog
## v6.0.2
### 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
### Bug fixes
* Fix upgrade.sh when Peertube is installed outside the standard path [#6064](https://github.com/Chocobozzz/PeerTube/pull/6064)
* Fix importing videos with too long chapter name
* Don't create chapters from description if there is only one
* Ensure user is owned by the auth plugin before updating its attributes
* Improve channels and accounts SEO by fixing structured JSON-LD data and canonical URLs
* Originally published and reupload date format consistency in watch page
* Fix cpu count when cpu info not available
* Fix embed when waiting for a live
* Fix updating already started live if live attributes don't change
* Fix displaying many countries in video stats
## v6.0.1 ## v6.0.1
### IMPORTANT NOTES ### IMPORTANT NOTES
@ -8,7 +29,7 @@
* We've made some modifications in v6.0.0 IMPORTANT NOTES, so if you upgrade from PeerTube v6.0.0: * We've made some modifications in v6.0.0 IMPORTANT NOTES, so if you upgrade from PeerTube v6.0.0:
* Ensure `location = /api/v1/videos/upload-resumable {` has been replaced by `location ~ ^/api/v1/videos/(upload-resumable|([^/]+/source/replace-resumable))$ {` in your nginx configuration * Ensure `location = /api/v1/videos/upload-resumable {` has been replaced by `location ~ ^/api/v1/videos/(upload-resumable|([^/]+/source/replace-resumable))$ {` in your nginx configuration
* Ensure you updated `storage.web_videos` configuration value to use `web-videos/` directory name * Ensure you updated `storage.web_videos` configuration value to use `web-videos/` directory name
* Ensure your directory name on filesystem is the same as `storage.web_videos` configuration * Ensure your directory name on filesystem is the same as `storage.web_videos` configuration value: directory on filesystem must be renamed from `videos/` to `web-videos/` to represent the value of `storage.web_videos`
### Bug fixes ### Bug fixes
@ -43,7 +64,7 @@ We have many important notes in this release. We know it's a pain for sysadmin,
* Directory on filesystem must be **renamed** from `videos/` to `web-videos/` to represent the value of `storage.web_videos` * Directory on filesystem must be **renamed** from `videos/` to `web-videos/` to represent the value of `storage.web_videos`
* Classic installation: `sudo -u peertube mv '/var/www/peertube/storage/videos/' '/var/www/peertube/storage/web-videos/'` * Classic installation: `sudo -u peertube mv '/var/www/peertube/storage/videos/' '/var/www/peertube/storage/web-videos/'`
* Docker installation: `mv '/path-to-docker-installation/docker-volume/data/videos/' '/path-to-docker-installation/docker-volume/data/web-videos/'` * Docker installation: `mv '/path-to-docker-installation/docker-volume/data/videos/' '/path-to-docker-installation/docker-volume/data/web-videos/'`
* `transcoding.webtorrent` must be **renamed** to `transcoding.web_videos`: https://github.com/Chocobozzz/PeerTube/blob/develop/config/production.yaml.example#L522 * `transcoding.webtorrent` must be **renamed** to `transcoding.web_videos`: https://github.com/Chocobozzz/PeerTube/blob/develop/config/production.yaml.example#L532
* `object_storage.videos` must be **renamed** to `object_storage.web_videos`. The value of `object_storage.web_videos.bucket_name` doesn't need to be changed: https://github.com/Chocobozzz/PeerTube/blob/develop/config/production.yaml.example#L223 * `object_storage.videos` must be **renamed** to `object_storage.web_videos`. The value of `object_storage.web_videos.bucket_name` doesn't need to be changed: https://github.com/Chocobozzz/PeerTube/blob/develop/config/production.yaml.example#L223
* `storage.storyboards` must be **added**: https://github.com/Chocobozzz/PeerTube/blob/develop/config/production.yaml.example#L157 * `storage.storyboards` must be **added**: https://github.com/Chocobozzz/PeerTube/blob/develop/config/production.yaml.example#L157
@ -61,7 +82,7 @@ We have many important notes in this release. We know it's a pain for sysadmin,
* `location ~ ^(/static/(webseed|streaming-playlists)/private/)|^/download {` must be updated to `location ~ ^(/static/(webseed|web-videos|streaming-playlists)/private/)|^/download {` * `location ~ ^(/static/(webseed|streaming-playlists)/private/)|^/download {` must be updated to `location ~ ^(/static/(webseed|web-videos|streaming-playlists)/private/)|^/download {`
* `location ~ ^/static/(webseed|redundancy|streaming-playlists)/ {` must be updated to `location ~ ^/static/(webseed|web-videos|redundancy|streaming-playlists)/ {` * `location ~ ^/static/(webseed|redundancy|streaming-playlists)/ {` must be updated to `location ~ ^/static/(webseed|web-videos|redundancy|streaming-playlists)/ {`
* Tracing requires `--experimental-loader=@opentelemetry/instrumentation/hook.mjs` node option: https://github.com/Chocobozzz/PeerTube/blob/develop/config/production.yaml.example#L263 * Tracing requires `--experimental-loader=@opentelemetry/instrumentation/hook.mjs` node option: https://github.com/Chocobozzz/PeerTube/blob/develop/config/production.yaml.example#L264
#### Developers important notes #### Developers important notes

View File

@ -1,6 +1,6 @@
{ {
"name": "peertube-client", "name": "peertube-client",
"version": "6.0.1", "version": "6.0.2",
"private": true, "private": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"author": { "author": {

View File

@ -45,7 +45,7 @@
</a> </a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<div class="chart-container" [ngStyle]="{ 'min-height': chartHeight }"> <div class="chart-container">
<p-chart <p-chart
*ngIf="chartOptions[availableChart.id]" *ngIf="chartOptions[availableChart.id]"
[height]="chartHeight" [width]="chartWidth" [height]="chartHeight" [width]="chartWidth"

View File

@ -1,4 +1,4 @@
import { ChartConfiguration, ChartData, PluginOptionsByType, Scale, TooltipItem } from 'chart.js' import { ChartConfiguration, ChartData, ChartOptions, PluginOptionsByType, Scale, TooltipItem } from 'chart.js'
import zoomPlugin from 'chartjs-plugin-zoom' import zoomPlugin from 'chartjs-plugin-zoom'
import { Observable, of } from 'rxjs' import { Observable, of } from 'rxjs'
import { SelectOptionsItem } from 'src/types' import { SelectOptionsItem } from 'src/types'
@ -25,6 +25,9 @@ type CountryData = { name: string, viewers: number }[]
type ChartIngestData = VideoStatsTimeserie | VideoStatsRetention | CountryData type ChartIngestData = VideoStatsTimeserie | VideoStatsRetention | CountryData
type ChartBuilderResult = { type ChartBuilderResult = {
type: 'line' | 'bar' type: 'line' | 'bar'
options?: ChartOptions<'bar'>
plugins: Partial<PluginOptionsByType<'line' | 'bar'>> plugins: Partial<PluginOptionsByType<'line' | 'bar'>>
data: ChartData<'line' | 'bar'> data: ChartData<'line' | 'bar'>
displayLegend: boolean displayLegend: boolean
@ -136,6 +139,12 @@ export class VideoStatsComponent implements OnInit {
onChartChange (newActive: ActiveGraphId) { onChartChange (newActive: ActiveGraphId) {
this.activeGraphId = newActive this.activeGraphId = newActive
if (newActive === 'countries') {
this.chartHeight = `${Math.max(this.countries.length * 20, 300)}px`
} else {
this.chartHeight = '300px'
}
this.loadChart() this.loadChart()
} }
@ -333,7 +342,7 @@ export class VideoStatsComponent implements OnInit {
countries: (rawData: CountryData) => this.buildCountryChartOptions(rawData) countries: (rawData: CountryData) => this.buildCountryChartOptions(rawData)
} }
const { type, data, displayLegend, plugins } = dataBuilders[graphId](this.chartIngestData[graphId]) const { type, data, displayLegend, plugins, options } = dataBuilders[graphId](this.chartIngestData[graphId])
const self = this const self = this
@ -342,6 +351,8 @@ export class VideoStatsComponent implements OnInit {
data, data,
options: { options: {
...options,
responsive: true, responsive: true,
scales: { scales: {
@ -366,7 +377,9 @@ export class VideoStatsComponent implements OnInit {
: undefined, : undefined,
ticks: { ticks: {
callback: value => this.formatYTick({ graphId, value }) callback: function (value) {
return self.formatYTick({ graphId, value, scale: this })
}
} }
} }
}, },
@ -489,6 +502,10 @@ export class VideoStatsComponent implements OnInit {
return { return {
type: 'bar' as 'bar', type: 'bar' as 'bar',
options: {
indexAxis: 'y'
},
displayLegend: true, displayLegend: true,
plugins: { plugins: {
@ -547,11 +564,13 @@ export class VideoStatsComponent implements OnInit {
private formatYTick (options: { private formatYTick (options: {
graphId: ActiveGraphId graphId: ActiveGraphId
value: number | string value: number | string
scale?: Scale
}) { }) {
const { graphId, value } = options const { graphId, value, scale } = options
if (graphId === 'retention') return value + ' %' if (graphId === 'retention') return value + ' %'
if (graphId === 'aggregateWatchTime') return secondsToTime(+value) if (graphId === 'aggregateWatchTime') return secondsToTime(+value)
if (graphId === 'countries' && scale) return scale.getLabelForValue(value as number)
return value.toLocaleString(this.localeId) return value.toLocaleString(this.localeId)
} }

View File

@ -21,7 +21,7 @@ import {
} from '@app/shared/shared-main' } from '@app/shared/shared-main'
import { LiveVideoService } from '@app/shared/shared-video-live' import { LiveVideoService } from '@app/shared/shared-video-live'
import { LoadingBarService } from '@ngx-loading-bar/core' import { LoadingBarService } from '@ngx-loading-bar/core'
import { pick, simpleObjectsDeepEqual } from '@peertube/peertube-core-utils' import { simpleObjectsDeepEqual } from '@peertube/peertube-core-utils'
import { HttpStatusCode, LiveVideo, LiveVideoUpdate, VideoPrivacy, VideoSource, VideoState } from '@peertube/peertube-models' import { HttpStatusCode, LiveVideo, LiveVideoUpdate, VideoPrivacy, VideoSource, VideoState } from '@peertube/peertube-models'
import { hydrateFormFromVideo } from './shared/video-edit-utils' import { hydrateFormFromVideo } from './shared/video-edit-utils'
import { VideoUploadService } from './shared/video-upload.service' import { VideoUploadService } from './shared/video-upload.service'
@ -221,7 +221,12 @@ export class VideoUpdateComponent extends FormReactive implements OnInit, OnDest
} }
// Don't update live attributes if they did not change // Don't update live attributes if they did not change
const baseVideo = pick(this.liveVideo, Object.keys(liveVideoUpdate) as (keyof LiveVideoUpdate)[]) const baseVideo = {
saveReplay: this.liveVideo.saveReplay,
replaySettings: this.liveVideo.replaySettings,
permanentLive: this.liveVideo.permanentLive,
latencyMode: this.liveVideo.latencyMode
}
const liveChanged = !simpleObjectsDeepEqual(baseVideo, liveVideoUpdate) const liveChanged = !simpleObjectsDeepEqual(baseVideo, liveVideoUpdate)
if (!liveChanged) return of(undefined) if (!liveChanged) return of(undefined)

View File

@ -35,7 +35,7 @@
<div *ngIf="!!video.originallyPublishedAt" class="attribute attribute-originally-published-at"> <div *ngIf="!!video.originallyPublishedAt" class="attribute attribute-originally-published-at">
<span i18n class="attribute-label">Originally published</span> <span i18n class="attribute-label">Originally published</span>
<span class="attribute-value">{{ video.originallyPublishedAt | date: 'dd MMMM yyyy' }}</span> <span class="attribute-value">{{ video.originallyPublishedAt | date: 'shortDate' }}</span>
</div> </div>
<div class="attribute attribute-category"> <div class="attribute attribute-category">

View File

@ -14,7 +14,7 @@ export class WebVideoOptionsBuilder {
videoFiles: this.options.webVideo.videoFiles.length !== 0 videoFiles: this.options.webVideo.videoFiles.length !== 0
? this.options.webVideo.videoFiles ? this.options.webVideo.videoFiles
: this.options?.hls.videoFiles || [] : this.options.hls?.videoFiles || []
} }
} }
} }

View File

@ -27,7 +27,8 @@ class WebVideoPlugin extends Plugin {
this.videoFiles = options.videoFiles this.videoFiles = options.videoFiles
this.videoFileToken = options.videoFileToken this.videoFileToken = options.videoFileToken
this.updateVideoFile({ videoFile: this.pickAverageVideoFile(), isUserResolutionChange: false }) const videoFile = this.pickAverageVideoFile()
if (videoFile) this.updateVideoFile({ videoFile, isUserResolutionChange: false })
this.onLoadedMetadata = () => { this.onLoadedMetadata = () => {
player.trigger('video-ratio-changed', { ratio: this.player.videoWidth() / this.player.videoHeight() }) player.trigger('video-ratio-changed', { ratio: this.player.videoWidth() / this.player.videoHeight() })
@ -36,19 +37,19 @@ class WebVideoPlugin extends Plugin {
player.on('loadedmetadata', this.onLoadedMetadata) player.on('loadedmetadata', this.onLoadedMetadata)
player.ready(() => { player.ready(() => {
this.buildQualities()
this.setupNetworkInfoInterval()
if (this.videoFiles.length === 0) { if (this.videoFiles.length === 0) {
this.player.addClass('disabled') this.player.addClass('disabled')
return return
} }
this.buildQualities()
this.setupNetworkInfoInterval()
}) })
} }
dispose () { dispose () {
clearInterval(this.networkInfoInterval) if (this.networkInfoInterval) clearInterval(this.networkInfoInterval)
if (this.onLoadedMetadata) this.player.off('loadedmetadata', this.onLoadedMetadata) if (this.onLoadedMetadata) this.player.off('loadedmetadata', this.onLoadedMetadata)
if (this.onErrorHandler) this.player.off('error', this.onErrorHandler) if (this.onErrorHandler) this.player.off('error', this.onErrorHandler)
@ -58,7 +59,7 @@ class WebVideoPlugin extends Plugin {
} }
getCurrentResolutionId () { getCurrentResolutionId () {
return this.currentVideoFile.resolution.id return this.currentVideoFile?.resolution.id
} }
updateVideoFile (options: { updateVideoFile (options: {
@ -123,7 +124,7 @@ class WebVideoPlugin extends Plugin {
private adaptPosterForAudioOnly () { private adaptPosterForAudioOnly () {
// Audio-only (resolutionId === 0) gets special treatment // Audio-only (resolutionId === 0) gets special treatment
if (this.currentVideoFile.resolution.id === 0) { if (this.currentVideoFile?.resolution.id === 0) {
this.player.audioPosterMode(true) this.player.audioPosterMode(true)
} else { } else {
this.player.audioPosterMode(false) this.player.audioPosterMode(false)
@ -154,6 +155,7 @@ class WebVideoPlugin extends Plugin {
} }
private pickAverageVideoFile () { private pickAverageVideoFile () {
if (!this.videoFiles || this.videoFiles.length === 0) return undefined
if (this.videoFiles.length === 1) return this.videoFiles[0] if (this.videoFiles.length === 1) return this.videoFiles[0]
const files = this.videoFiles.filter(f => f.resolution.id !== 0) const files = this.videoFiles.filter(f => f.resolution.id !== 0)
@ -165,7 +167,7 @@ class WebVideoPlugin extends Plugin {
id: videoFile.resolution.id, id: videoFile.resolution.id,
label: this.buildQualityLabel(videoFile), label: this.buildQualityLabel(videoFile),
height: videoFile.resolution.id, height: videoFile.resolution.id,
selected: videoFile.id === this.currentVideoFile.id, selected: videoFile.id === this.currentVideoFile?.id,
selectCallback: () => this.updateVideoFile({ videoFile, isUserResolutionChange: true }) selectCallback: () => this.updateVideoFile({ videoFile, isUserResolutionChange: true })
})) }))
@ -187,7 +189,7 @@ class WebVideoPlugin extends Plugin {
return this.player.trigger('network-info', { return this.player.trigger('network-info', {
source: 'web-video', source: 'web-video',
http: { http: {
downloaded: this.player.bufferedPercent() * this.currentVideoFile.size downloaded: this.player.bufferedPercent() * this.currentVideoFile?.size
} }
} as PlayerNetworkInfo) } as PlayerNetworkInfo)
}, 1000) }, 1000)

View File

@ -1,7 +1,7 @@
{ {
"name": "peertube", "name": "peertube",
"description": "PeerTube, an ActivityPub-federated video streaming platform using P2P directly in your web browser.", "description": "PeerTube, an ActivityPub-federated video streaming platform using P2P directly in your web browser.",
"version": "6.0.1", "version": "6.0.2",
"private": true, "private": true,
"licence": "AGPL-3.0", "licence": "AGPL-3.0",
"engines": { "engines": {

View File

@ -2,7 +2,7 @@ import { timeToInt, timecodeRegexString } from '../common/date.js'
const timecodeRegex = new RegExp(`^(${timecodeRegexString})\\s`) const timecodeRegex = new RegExp(`^(${timecodeRegexString})\\s`)
export function parseChapters (text: string) { export function parseChapters (text: string, maxTitleLength: number) {
if (!text) return [] if (!text) return []
const lines = text.split(/\r?\n|\r|\n/g) const lines = text.split(/\r?\n|\r|\n/g)
@ -25,8 +25,11 @@ export function parseChapters (text: string) {
const timecode = timeToInt(timecodeText) const timecode = timeToInt(timecodeText)
const title = line.replace(matched[0], '') const title = line.replace(matched[0], '')
chapters.push({ timecode, title }) chapters.push({ timecode, title: title.slice(0, maxTitleLength) })
} }
return chapters // Only consider chapters if there are more than one
if (chapters.length > 1) return chapters
return []
} }

View File

@ -178,13 +178,13 @@ describe('Test video chapters', function () {
checkChapters(chapters) checkChapters(chapters)
} }
await servers[0].videos.update({ id: video.uuid, attributes: { description: '00:01 chapter 1' } }) await servers[0].videos.update({ id: video.uuid, attributes: { description: '00:01 chapter 1\n00:03 chapter 2' } })
await waitJobs(servers) await waitJobs(servers)
for (const server of servers) { for (const server of servers) {
const { chapters } = await server.chapters.list({ videoId: video.uuid }) const { chapters } = await server.chapters.list({ videoId: video.uuid })
expect(chapters).to.deep.equal([ { timecode: 1, title: 'chapter 1' } ]) expect(chapters).to.deep.equal([ { timecode: 1, title: 'chapter 1' }, { timecode: 3, title: 'chapter 2' } ])
} }
await servers[0].videos.update({ id: video.uuid, attributes: { description: 'null description' } }) await servers[0].videos.update({ id: video.uuid, attributes: { description: 'null description' } })

View File

@ -103,7 +103,7 @@ describe('Test index HTML generation', function () {
it('Should use the original account URL for the canonical tag', async function () { it('Should use the original account URL for the canonical tag', async function () {
const accountURLtest = res => { const accountURLtest = res => {
expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/a/root" />`) expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/a/root/video-channels" />`)
} }
accountURLtest(await makeHTMLRequest(servers[0].url, '/accounts/root@' + servers[0].host)) accountURLtest(await makeHTMLRequest(servers[0].url, '/accounts/root@' + servers[0].host))
@ -113,7 +113,7 @@ describe('Test index HTML generation', function () {
it('Should use the original channel URL for the canonical tag', async function () { it('Should use the original channel URL for the canonical tag', async function () {
const channelURLtests = res => { const channelURLtests = res => {
expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/c/root_channel" />`) expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/c/root_channel/videos" />`)
} }
channelURLtests(await makeHTMLRequest(servers[0].url, '/video-channels/root_channel@' + servers[0].host)) channelURLtests(await makeHTMLRequest(servers[0].url, '/video-channels/root_channel@' + servers[0].host))

View File

@ -48,7 +48,7 @@ describe('Test Open Graph and Twitter cards HTML tags', function () {
expect(text).to.contain(`<meta property="og:title" content="${account.displayName}" />`) expect(text).to.contain(`<meta property="og:title" content="${account.displayName}" />`)
expect(text).to.contain(`<meta property="og:description" content="${account.description}" />`) expect(text).to.contain(`<meta property="og:description" content="${account.description}" />`)
expect(text).to.contain('<meta property="og:type" content="website" />') expect(text).to.contain('<meta property="og:type" content="website" />')
expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/a/${servers[0].store.user.username}" />`) expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/a/${servers[0].store.user.username}/video-channels" />`)
} }
async function channelPageTest (path: string) { async function channelPageTest (path: string) {
@ -58,7 +58,7 @@ describe('Test Open Graph and Twitter cards HTML tags', function () {
expect(text).to.contain(`<meta property="og:title" content="${servers[0].store.channel.displayName}" />`) expect(text).to.contain(`<meta property="og:title" content="${servers[0].store.channel.displayName}" />`)
expect(text).to.contain(`<meta property="og:description" content="${channelDescription}" />`) expect(text).to.contain(`<meta property="og:description" content="${channelDescription}" />`)
expect(text).to.contain('<meta property="og:type" content="website" />') expect(text).to.contain('<meta property="og:type" content="website" />')
expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/c/${servers[0].store.channel.name}" />`) expect(text).to.contain(`<meta property="og:url" content="${servers[0].url}/c/${servers[0].store.channel.name}/videos" />`)
} }
async function watchVideoPageTest (path: string) { async function watchVideoPageTest (path: string) {

View File

@ -212,11 +212,11 @@ describe('Test misc endpoints', function () {
expect(res.text).to.contain('<video:title>video 2</video:title>') expect(res.text).to.contain('<video:title>video 2</video:title>')
expect(res.text).to.not.contain('<video:title>video 3</video:title>') expect(res.text).to.not.contain('<video:title>video 3</video:title>')
expect(res.text).to.contain('<url><loc>' + server.url + '/c/channel1</loc></url>') expect(res.text).to.contain('<url><loc>' + server.url + '/c/channel1/videos</loc></url>')
expect(res.text).to.contain('<url><loc>' + server.url + '/c/channel2</loc></url>') expect(res.text).to.contain('<url><loc>' + server.url + '/c/channel2/videos</loc></url>')
expect(res.text).to.contain('<url><loc>' + server.url + '/a/user1</loc></url>') expect(res.text).to.contain('<url><loc>' + server.url + '/a/user1/video-channels</loc></url>')
expect(res.text).to.contain('<url><loc>' + server.url + '/a/user2</loc></url>') expect(res.text).to.contain('<url><loc>' + server.url + '/a/user2/video-channels</loc></url>')
}) })
it('Should not fail with big title/description videos', async function () { it('Should not fail with big title/description videos', async function () {

View File

@ -242,6 +242,29 @@ describe('Test id and pass auth plugins', function () {
expect(laguna.pluginAuth).to.equal('peertube-plugin-test-id-pass-auth-two') expect(laguna.pluginAuth).to.equal('peertube-plugin-test-id-pass-auth-two')
}) })
it('Should not update a user if not owned by the plugin auth', async function () {
{
await server.users.update({ userId: lagunaId, videoQuota: 43000, password: 'coucou', pluginAuth: null })
const body = await server.users.get({ userId: lagunaId })
expect(body.videoQuota).to.equal(43000)
expect(body.pluginAuth).to.be.null
}
{
await server.login.login({
user: { username: 'laguna', password: 'laguna password' },
expectedStatus: HttpStatusCode.BAD_REQUEST_400
})
}
{
const body = await server.users.get({ userId: lagunaId })
expect(body.videoQuota).to.equal(43000)
expect(body.pluginAuth).to.be.null
}
})
after(async function () { after(async function () {
await cleanupTests([ server ]) await cleanupTests([ server ])
}) })

View File

@ -221,25 +221,38 @@ describe('Parse semantic version string', function () {
describe('Extract chapters', function () { describe('Extract chapters', function () {
it('Should not extract chapters', function () { it('Should not extract chapters', function () {
expect(parseChapters('my super description\nno?')).to.deep.equal([]) expect(parseChapters('my super description\nno?', 100)).to.deep.equal([])
expect(parseChapters('m00:00 super description\nno?')).to.deep.equal([]) expect(parseChapters('m00:00 super description\nno?', 100)).to.deep.equal([])
expect(parseChapters('00:00super description\nno?')).to.deep.equal([]) expect(parseChapters('00:00super description\nno?', 100)).to.deep.equal([])
expect(parseChapters('my super description\n'.repeat(10) + ' * list1\n * list 2\n * list 3')).to.deep.equal([]) expect(parseChapters('my super description\n'.repeat(10) + ' * list1\n * list 2\n * list 3', 100)).to.deep.equal([])
expect(parseChapters('3 Hello coucou', 100)).to.deep.equal([])
expect(parseChapters('00:00 coucou', 100)).to.deep.equal([])
}) })
it('Should extract chapters', function () { it('Should extract chapters', function () {
expect(parseChapters('00:00 coucou')).to.deep.equal([ { timecode: 0, title: 'coucou' } ]) expect(parseChapters('00:00 coucou\n00:05 hello', 100)).to.deep.equal([
expect(parseChapters('my super description\n\n00:01:30 chapter 1\n00:01:35 chapter 2')).to.deep.equal([ { timecode: 0, title: 'coucou' },
{ timecode: 5, title: 'hello' }
])
expect(parseChapters('my super description\n\n00:01:30 chapter 1\n00:01:35 chapter 2', 100)).to.deep.equal([
{ timecode: 90, title: 'chapter 1' }, { timecode: 90, title: 'chapter 1' },
{ timecode: 95, title: 'chapter 2' } { timecode: 95, title: 'chapter 2' }
]) ])
expect(parseChapters('hi\n\n00:01:30 chapter 1\n00:01:35 chapter 2\nhi')).to.deep.equal([ expect(parseChapters('hi\n\n00:01:30 chapter 1\n00:01:35 chapter 2\nhi', 100)).to.deep.equal([
{ timecode: 90, title: 'chapter 1' }, { timecode: 90, title: 'chapter 1' },
{ timecode: 95, title: 'chapter 2' } { timecode: 95, title: 'chapter 2' }
]) ])
expect(parseChapters('hi\n\n00:01:30 chapter 1\n00:01:35 chapter 2\nhi\n00:01:40 chapter 3')).to.deep.equal([ expect(parseChapters('hi\n\n00:01:30 chapter 1\n00:01:35 chapter 2\nhi\n00:01:40 chapter 3', 100)).to.deep.equal([
{ timecode: 90, title: 'chapter 1' }, { timecode: 90, title: 'chapter 1' },
{ timecode: 95, title: 'chapter 2' } { timecode: 95, title: 'chapter 2' }
]) ])
}) })
it('Should respect the max length option', function () {
expect(parseChapters('my super description\n\n00:01:30 chapter 1\n00:01:35 chapter 2', 3)).to.deep.equal([
{ timecode: 90, title: 'cha' },
{ timecode: 95, title: 'cha' }
])
})
}) })

View File

@ -4,4 +4,5 @@ set -eu
# Backward path compatibility now upgrade.sh is in dist/scripts since v6 # Backward path compatibility now upgrade.sh is in dist/scripts since v6
/bin/sh ../dist/scripts/upgrade.sh /bin/sh ../dist/scripts/upgrade.sh ${1:-/var/www/peertube}

View File

@ -968,7 +968,7 @@ const MEMOIZE_LENGTH = {
VIDEO_DURATION: 200 VIDEO_DURATION: 200
} }
const totalCPUs = cpus().length const totalCPUs = Math.max(cpus().length, 1)
const WORKER_THREADS = { const WORKER_THREADS = {
DOWNLOAD_IMAGE: { DOWNLOAD_IMAGE: {

View File

@ -89,8 +89,11 @@ async function getUser (usernameOrEmail?: string, password?: string, bypassLogin
let user = await UserModel.loadByEmail(bypassLogin.user.email) let user = await UserModel.loadByEmail(bypassLogin.user.email)
if (!user) user = await createUserFromExternal(bypassLogin.pluginName, bypassLogin.user) if (!user) {
else user = await updateUserFromExternal(user, bypassLogin.user, bypassLogin.userUpdater) user = await createUserFromExternal(bypassLogin.pluginName, bypassLogin.user)
} else if (user.pluginAuth === bypassLogin.pluginName) {
user = await updateUserFromExternal(user, bypassLogin.user, bypassLogin.userUpdater)
}
// Cannot create a user // Cannot create a user
if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.') if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.')

View File

@ -80,6 +80,10 @@ export class ActorHtml {
ogType, ogType,
twitterCard, twitterCard,
schemaType, schemaType,
jsonldProfile: {
createdAt: entity.createdAt,
updatedAt: entity.updatedAt
},
indexationPolicy: entity.Actor.isOwned() indexationPolicy: entity.Actor.isOwned()
? 'always' ? 'always'

View File

@ -11,10 +11,16 @@ type Tags = {
url?: string url?: string
schemaType?: string
ogType?: string ogType?: string
twitterCard?: 'player' | 'summary' | 'summary_large_image' twitterCard?: 'player' | 'summary' | 'summary_large_image'
schemaType?: string
jsonldProfile?: {
createdAt: Date
updatedAt: Date
}
list?: { list?: {
numberOfItems: number numberOfItems: number
} }
@ -195,6 +201,28 @@ export class TagsHtml {
static generateSchemaTagsOptions (tags: Tags, context: HookContext) { static generateSchemaTagsOptions (tags: Tags, context: HookContext) {
if (!tags.schemaType) return if (!tags.schemaType) return
if (tags.schemaType === 'ProfilePage') {
if (!tags.jsonldProfile) throw new Error('Missing `jsonldProfile` with ProfilePage schema type')
const profilePageSchema = {
'@context': 'http://schema.org',
'@type': tags.schemaType,
'dateCreated': tags.jsonldProfile.createdAt.toISOString(),
'dateModified': tags.jsonldProfile.updatedAt.toISOString(),
'mainEntity': {
'@id': '#main-author',
'@type': 'Person',
'name': tags.escapedTitle,
'description': tags.escapedTruncatedDescription,
'image': tags.image.url
}
}
return Hooks.wrapObject(profilePageSchema, 'filter:html.client.json-ld.result', context)
}
const schema = { const schema = {
'@context': 'http://schema.org', '@context': 'http://schema.org',
'@type': tags.schemaType, '@type': tags.schemaType,

View File

@ -38,7 +38,7 @@ export async function moveToJob (options: {
} }
if (video.VideoStreamingPlaylists) { if (video.VideoStreamingPlaylists) {
logger.debug('Moving HLS playlist of %s.', video.uuid) logger.debug('Moving HLS playlist of %s.', video.uuid, lTags)
await moveHLSFiles(video) await moveHLSFiles(video)
} }

View File

@ -68,7 +68,9 @@ export class FFmpegTranscodingWrapper extends AbstractTranscodingWrapper {
logger.debug('Killing ffmpeg after live abort of ' + this.videoUUID, this.lTags()) logger.debug('Killing ffmpeg after live abort of ' + this.videoUUID, this.lTags())
this.ffmpegCommand.kill('SIGINT') if (this.ffmpegCommand) {
this.ffmpegCommand.kill('SIGINT')
}
this.aborted = true this.aborted = true
this.emit('end') this.emit('end')

View File

@ -5,6 +5,7 @@ import { VideoChapterModel } from '@server/models/video/video-chapter.js'
import { MVideoImmutable } from '@server/types/models/index.js' import { MVideoImmutable } from '@server/types/models/index.js'
import { Transaction } from 'sequelize' import { Transaction } from 'sequelize'
import { InternalEventEmitter } from './internal-event-emitter.js' import { InternalEventEmitter } from './internal-event-emitter.js'
import { CONSTRAINTS_FIELDS } from '@server/initializers/constants.js'
const lTags = loggerTagsFactory('video', 'chapters') const lTags = loggerTagsFactory('video', 'chapters')
@ -44,7 +45,7 @@ export async function replaceChaptersFromDescriptionIfNeeded (options: {
}) { }) {
const { transaction, video, newDescription, oldDescription = '' } = options const { transaction, video, newDescription, oldDescription = '' } = options
const chaptersFromOldDescription = sortBy(parseChapters(oldDescription), 'timecode') const chaptersFromOldDescription = sortBy(parseChapters(oldDescription, CONSTRAINTS_FIELDS.VIDEO_CHAPTERS.TITLE.max), 'timecode')
const existingChapters = await VideoChapterModel.listChaptersOfVideo(video.id, transaction) const existingChapters = await VideoChapterModel.listChaptersOfVideo(video.id, transaction)
logger.debug( logger.debug(
@ -54,7 +55,7 @@ export async function replaceChaptersFromDescriptionIfNeeded (options: {
// Then we can update chapters from the new description // Then we can update chapters from the new description
if (areSameChapters(chaptersFromOldDescription, existingChapters)) { if (areSameChapters(chaptersFromOldDescription, existingChapters)) {
const chaptersFromNewDescription = sortBy(parseChapters(newDescription), 'timecode') const chaptersFromNewDescription = sortBy(parseChapters(newDescription, CONSTRAINTS_FIELDS.VIDEO_CHAPTERS.TITLE.max), 'timecode')
if (chaptersFromOldDescription.length === 0 && chaptersFromNewDescription.length === 0) return false if (chaptersFromOldDescription.length === 0 && chaptersFromNewDescription.length === 0) return false
await replaceChapters({ video, chapters: chaptersFromNewDescription, transaction }) await replaceChapters({ video, chapters: chaptersFromNewDescription, transaction })

View File

@ -457,7 +457,7 @@ export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> {
// Avoid error when running this method on MAccount... | MChannel... // Avoid error when running this method on MAccount... | MChannel...
getClientUrl (this: MAccountHost | MChannelHost) { getClientUrl (this: MAccountHost | MChannelHost) {
return WEBSERVER.URL + '/a/' + this.Actor.getIdentifier() return WEBSERVER.URL + '/a/' + this.Actor.getIdentifier() + '/video-channels'
} }
isBlocked () { isBlocked () {

View File

@ -873,6 +873,8 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> {
} }
isPasswordMatch (password: string) { isPasswordMatch (password: string) {
if (!password || !this.password) return false
return comparePassword(password, this.password) return comparePassword(password, this.password)
} }

View File

@ -841,7 +841,7 @@ export class VideoChannelModel extends Model<Partial<AttributesOnly<VideoChannel
// Avoid error when running this method on MAccount... | MChannel... // Avoid error when running this method on MAccount... | MChannel...
getClientUrl (this: MAccountHost | MChannelHost) { getClientUrl (this: MAccountHost | MChannelHost) {
return WEBSERVER.URL + '/c/' + this.Actor.getIdentifier() return WEBSERVER.URL + '/c/' + this.Actor.getIdentifier() + '/videos'
} }
getDisplayName () { getDisplayName () {

View File

@ -9773,10 +9773,10 @@ components:
description: P2P peers connected (doesn't include WebSeed peers) description: P2P peers connected (doesn't include WebSeed peers)
resolutionChanges: resolutionChanges:
type: number type: number
description: How many resolution changes occured since the last metric creation description: How many resolution changes occurred since the last metric creation
errors: errors:
type: number type: number
description: How many errors occured since the last metric creation description: How many errors occurred since the last metric creation
downloadedBytesP2P: downloadedBytesP2P:
type: number type: number
description: How many bytes were downloaded with P2P since the last metric creation description: How many bytes were downloaded with P2P since the last metric creation

View File

@ -870,7 +870,7 @@ function register ({ registerClientRoute }) {
} }
``` ```
You can then access the page on `/p/my-super/route` (please note the additionnal `/p/` in the path). You can then access the page on `/p/my-super/route` (please note the additional `/p/` in the path).
### Publishing ### Publishing

View File

@ -185,7 +185,7 @@ peertube-runner list-registered
## Server tools ## Server tools
Server tools are scripts that interect directly with the code of your PeerTube instance. Server tools are scripts that interact directly with the code of your PeerTube instance.
They must be run on the server, in `peertube-latest` directory. They must be run on the server, in `peertube-latest` directory.
### Parse logs ### Parse logs