v6.0.2
-----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:
commit
3438b07487
27
CHANGELOG.md
27
CHANGELOG.md
|
@ -1,5 +1,26 @@
|
|||
# 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
|
||||
|
||||
### 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:
|
||||
* 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 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
|
||||
|
||||
|
@ -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`
|
||||
* 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/'`
|
||||
* `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
|
||||
* `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|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
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "peertube-client",
|
||||
"version": "6.0.1",
|
||||
"version": "6.0.2",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"author": {
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
</a>
|
||||
|
||||
<ng-template ngbNavContent>
|
||||
<div class="chart-container" [ngStyle]="{ 'min-height': chartHeight }">
|
||||
<div class="chart-container">
|
||||
<p-chart
|
||||
*ngIf="chartOptions[availableChart.id]"
|
||||
[height]="chartHeight" [width]="chartWidth"
|
||||
|
|
|
@ -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 { Observable, of } from 'rxjs'
|
||||
import { SelectOptionsItem } from 'src/types'
|
||||
|
@ -25,6 +25,9 @@ type CountryData = { name: string, viewers: number }[]
|
|||
type ChartIngestData = VideoStatsTimeserie | VideoStatsRetention | CountryData
|
||||
type ChartBuilderResult = {
|
||||
type: 'line' | 'bar'
|
||||
|
||||
options?: ChartOptions<'bar'>
|
||||
|
||||
plugins: Partial<PluginOptionsByType<'line' | 'bar'>>
|
||||
data: ChartData<'line' | 'bar'>
|
||||
displayLegend: boolean
|
||||
|
@ -136,6 +139,12 @@ export class VideoStatsComponent implements OnInit {
|
|||
onChartChange (newActive: ActiveGraphId) {
|
||||
this.activeGraphId = newActive
|
||||
|
||||
if (newActive === 'countries') {
|
||||
this.chartHeight = `${Math.max(this.countries.length * 20, 300)}px`
|
||||
} else {
|
||||
this.chartHeight = '300px'
|
||||
}
|
||||
|
||||
this.loadChart()
|
||||
}
|
||||
|
||||
|
@ -333,7 +342,7 @@ export class VideoStatsComponent implements OnInit {
|
|||
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
|
||||
|
||||
|
@ -342,6 +351,8 @@ export class VideoStatsComponent implements OnInit {
|
|||
data,
|
||||
|
||||
options: {
|
||||
...options,
|
||||
|
||||
responsive: true,
|
||||
|
||||
scales: {
|
||||
|
@ -366,7 +377,9 @@ export class VideoStatsComponent implements OnInit {
|
|||
: undefined,
|
||||
|
||||
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 {
|
||||
type: 'bar' as 'bar',
|
||||
|
||||
options: {
|
||||
indexAxis: 'y'
|
||||
},
|
||||
|
||||
displayLegend: true,
|
||||
|
||||
plugins: {
|
||||
|
@ -547,11 +564,13 @@ export class VideoStatsComponent implements OnInit {
|
|||
private formatYTick (options: {
|
||||
graphId: ActiveGraphId
|
||||
value: number | string
|
||||
scale?: Scale
|
||||
}) {
|
||||
const { graphId, value } = options
|
||||
const { graphId, value, scale } = options
|
||||
|
||||
if (graphId === 'retention') return value + ' %'
|
||||
if (graphId === 'aggregateWatchTime') return secondsToTime(+value)
|
||||
if (graphId === 'countries' && scale) return scale.getLabelForValue(value as number)
|
||||
|
||||
return value.toLocaleString(this.localeId)
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
} from '@app/shared/shared-main'
|
||||
import { LiveVideoService } from '@app/shared/shared-video-live'
|
||||
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 { hydrateFormFromVideo } from './shared/video-edit-utils'
|
||||
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
|
||||
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)
|
||||
if (!liveChanged) return of(undefined)
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
|
||||
<div *ngIf="!!video.originallyPublishedAt" class="attribute attribute-originally-published-at">
|
||||
<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 class="attribute attribute-category">
|
||||
|
|
|
@ -14,7 +14,7 @@ export class WebVideoOptionsBuilder {
|
|||
|
||||
videoFiles: this.options.webVideo.videoFiles.length !== 0
|
||||
? this.options.webVideo.videoFiles
|
||||
: this.options?.hls.videoFiles || []
|
||||
: this.options.hls?.videoFiles || []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,8 @@ class WebVideoPlugin extends Plugin {
|
|||
this.videoFiles = options.videoFiles
|
||||
this.videoFileToken = options.videoFileToken
|
||||
|
||||
this.updateVideoFile({ videoFile: this.pickAverageVideoFile(), isUserResolutionChange: false })
|
||||
const videoFile = this.pickAverageVideoFile()
|
||||
if (videoFile) this.updateVideoFile({ videoFile, isUserResolutionChange: false })
|
||||
|
||||
this.onLoadedMetadata = () => {
|
||||
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.ready(() => {
|
||||
this.buildQualities()
|
||||
|
||||
this.setupNetworkInfoInterval()
|
||||
|
||||
if (this.videoFiles.length === 0) {
|
||||
this.player.addClass('disabled')
|
||||
return
|
||||
}
|
||||
|
||||
this.buildQualities()
|
||||
|
||||
this.setupNetworkInfoInterval()
|
||||
})
|
||||
}
|
||||
|
||||
dispose () {
|
||||
clearInterval(this.networkInfoInterval)
|
||||
if (this.networkInfoInterval) clearInterval(this.networkInfoInterval)
|
||||
|
||||
if (this.onLoadedMetadata) this.player.off('loadedmetadata', this.onLoadedMetadata)
|
||||
if (this.onErrorHandler) this.player.off('error', this.onErrorHandler)
|
||||
|
@ -58,7 +59,7 @@ class WebVideoPlugin extends Plugin {
|
|||
}
|
||||
|
||||
getCurrentResolutionId () {
|
||||
return this.currentVideoFile.resolution.id
|
||||
return this.currentVideoFile?.resolution.id
|
||||
}
|
||||
|
||||
updateVideoFile (options: {
|
||||
|
@ -123,7 +124,7 @@ class WebVideoPlugin extends Plugin {
|
|||
|
||||
private adaptPosterForAudioOnly () {
|
||||
// Audio-only (resolutionId === 0) gets special treatment
|
||||
if (this.currentVideoFile.resolution.id === 0) {
|
||||
if (this.currentVideoFile?.resolution.id === 0) {
|
||||
this.player.audioPosterMode(true)
|
||||
} else {
|
||||
this.player.audioPosterMode(false)
|
||||
|
@ -154,6 +155,7 @@ class WebVideoPlugin extends Plugin {
|
|||
}
|
||||
|
||||
private pickAverageVideoFile () {
|
||||
if (!this.videoFiles || this.videoFiles.length === 0) return undefined
|
||||
if (this.videoFiles.length === 1) return this.videoFiles[0]
|
||||
|
||||
const files = this.videoFiles.filter(f => f.resolution.id !== 0)
|
||||
|
@ -165,7 +167,7 @@ class WebVideoPlugin extends Plugin {
|
|||
id: videoFile.resolution.id,
|
||||
label: this.buildQualityLabel(videoFile),
|
||||
height: videoFile.resolution.id,
|
||||
selected: videoFile.id === this.currentVideoFile.id,
|
||||
selected: videoFile.id === this.currentVideoFile?.id,
|
||||
selectCallback: () => this.updateVideoFile({ videoFile, isUserResolutionChange: true })
|
||||
}))
|
||||
|
||||
|
@ -187,7 +189,7 @@ class WebVideoPlugin extends Plugin {
|
|||
return this.player.trigger('network-info', {
|
||||
source: 'web-video',
|
||||
http: {
|
||||
downloaded: this.player.bufferedPercent() * this.currentVideoFile.size
|
||||
downloaded: this.player.bufferedPercent() * this.currentVideoFile?.size
|
||||
}
|
||||
} as PlayerNetworkInfo)
|
||||
}, 1000)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "peertube",
|
||||
"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,
|
||||
"licence": "AGPL-3.0",
|
||||
"engines": {
|
||||
|
|
|
@ -2,7 +2,7 @@ import { timeToInt, timecodeRegexString } from '../common/date.js'
|
|||
|
||||
const timecodeRegex = new RegExp(`^(${timecodeRegexString})\\s`)
|
||||
|
||||
export function parseChapters (text: string) {
|
||||
export function parseChapters (text: string, maxTitleLength: number) {
|
||||
if (!text) return []
|
||||
|
||||
const lines = text.split(/\r?\n|\r|\n/g)
|
||||
|
@ -25,8 +25,11 @@ export function parseChapters (text: string) {
|
|||
const timecode = timeToInt(timecodeText)
|
||||
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 []
|
||||
}
|
||||
|
|
|
@ -178,13 +178,13 @@ describe('Test video chapters', function () {
|
|||
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)
|
||||
|
||||
for (const server of servers) {
|
||||
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' } })
|
||||
|
|
|
@ -103,7 +103,7 @@ describe('Test index HTML generation', function () {
|
|||
|
||||
it('Should use the original account URL for the canonical tag', async function () {
|
||||
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))
|
||||
|
@ -113,7 +113,7 @@ describe('Test index HTML generation', function () {
|
|||
|
||||
it('Should use the original channel URL for the canonical tag', async function () {
|
||||
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))
|
||||
|
|
|
@ -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:description" content="${account.description}" />`)
|
||||
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) {
|
||||
|
@ -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:description" content="${channelDescription}" />`)
|
||||
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) {
|
||||
|
|
|
@ -212,11 +212,11 @@ describe('Test misc endpoints', function () {
|
|||
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.contain('<url><loc>' + server.url + '/c/channel1</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/channel1/videos</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/user2</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/video-channels</loc></url>')
|
||||
})
|
||||
|
||||
it('Should not fail with big title/description videos', async function () {
|
||||
|
|
|
@ -242,6 +242,29 @@ describe('Test id and pass auth plugins', function () {
|
|||
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 () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
|
|
|
@ -221,25 +221,38 @@ describe('Parse semantic version string', function () {
|
|||
describe('Extract chapters', function () {
|
||||
|
||||
it('Should not extract chapters', function () {
|
||||
expect(parseChapters('my super description\nno?')).to.deep.equal([])
|
||||
expect(parseChapters('m00:00 super description\nno?')).to.deep.equal([])
|
||||
expect(parseChapters('00:00super description\nno?')).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\nno?', 100)).to.deep.equal([])
|
||||
expect(parseChapters('m00:00 super description\nno?', 100)).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', 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 () {
|
||||
expect(parseChapters('00:00 coucou')).to.deep.equal([ { timecode: 0, title: 'coucou' } ])
|
||||
expect(parseChapters('my super description\n\n00:01:30 chapter 1\n00:01:35 chapter 2')).to.deep.equal([
|
||||
expect(parseChapters('00:00 coucou\n00:05 hello', 100)).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: 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: 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: 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' }
|
||||
])
|
||||
})
|
||||
})
|
||||
|
|
|
@ -4,4 +4,5 @@ set -eu
|
|||
|
||||
# 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}
|
||||
|
||||
|
|
|
@ -968,7 +968,7 @@ const MEMOIZE_LENGTH = {
|
|||
VIDEO_DURATION: 200
|
||||
}
|
||||
|
||||
const totalCPUs = cpus().length
|
||||
const totalCPUs = Math.max(cpus().length, 1)
|
||||
|
||||
const WORKER_THREADS = {
|
||||
DOWNLOAD_IMAGE: {
|
||||
|
|
|
@ -89,8 +89,11 @@ async function getUser (usernameOrEmail?: string, password?: string, bypassLogin
|
|||
|
||||
let user = await UserModel.loadByEmail(bypassLogin.user.email)
|
||||
|
||||
if (!user) user = await createUserFromExternal(bypassLogin.pluginName, bypassLogin.user)
|
||||
else user = await updateUserFromExternal(user, bypassLogin.user, bypassLogin.userUpdater)
|
||||
if (!user) {
|
||||
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
|
||||
if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.')
|
||||
|
|
|
@ -80,6 +80,10 @@ export class ActorHtml {
|
|||
ogType,
|
||||
twitterCard,
|
||||
schemaType,
|
||||
jsonldProfile: {
|
||||
createdAt: entity.createdAt,
|
||||
updatedAt: entity.updatedAt
|
||||
},
|
||||
|
||||
indexationPolicy: entity.Actor.isOwned()
|
||||
? 'always'
|
||||
|
|
|
@ -11,10 +11,16 @@ type Tags = {
|
|||
|
||||
url?: string
|
||||
|
||||
schemaType?: string
|
||||
ogType?: string
|
||||
twitterCard?: 'player' | 'summary' | 'summary_large_image'
|
||||
|
||||
schemaType?: string
|
||||
|
||||
jsonldProfile?: {
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
list?: {
|
||||
numberOfItems: number
|
||||
}
|
||||
|
@ -195,6 +201,28 @@ export class TagsHtml {
|
|||
static generateSchemaTagsOptions (tags: Tags, context: HookContext) {
|
||||
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 = {
|
||||
'@context': 'http://schema.org',
|
||||
'@type': tags.schemaType,
|
||||
|
|
|
@ -38,7 +38,7 @@ export async function moveToJob (options: {
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -68,7 +68,9 @@ export class FFmpegTranscodingWrapper extends AbstractTranscodingWrapper {
|
|||
|
||||
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.emit('end')
|
||||
|
|
|
@ -5,6 +5,7 @@ import { VideoChapterModel } from '@server/models/video/video-chapter.js'
|
|||
import { MVideoImmutable } from '@server/types/models/index.js'
|
||||
import { Transaction } from 'sequelize'
|
||||
import { InternalEventEmitter } from './internal-event-emitter.js'
|
||||
import { CONSTRAINTS_FIELDS } from '@server/initializers/constants.js'
|
||||
|
||||
const lTags = loggerTagsFactory('video', 'chapters')
|
||||
|
||||
|
@ -44,7 +45,7 @@ export async function replaceChaptersFromDescriptionIfNeeded (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)
|
||||
|
||||
logger.debug(
|
||||
|
@ -54,7 +55,7 @@ export async function replaceChaptersFromDescriptionIfNeeded (options: {
|
|||
|
||||
// Then we can update chapters from the new description
|
||||
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
|
||||
|
||||
await replaceChapters({ video, chapters: chaptersFromNewDescription, transaction })
|
||||
|
|
|
@ -457,7 +457,7 @@ export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> {
|
|||
|
||||
// Avoid error when running this method on MAccount... | MChannel...
|
||||
getClientUrl (this: MAccountHost | MChannelHost) {
|
||||
return WEBSERVER.URL + '/a/' + this.Actor.getIdentifier()
|
||||
return WEBSERVER.URL + '/a/' + this.Actor.getIdentifier() + '/video-channels'
|
||||
}
|
||||
|
||||
isBlocked () {
|
||||
|
|
|
@ -873,6 +873,8 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> {
|
|||
}
|
||||
|
||||
isPasswordMatch (password: string) {
|
||||
if (!password || !this.password) return false
|
||||
|
||||
return comparePassword(password, this.password)
|
||||
}
|
||||
|
||||
|
|
|
@ -841,7 +841,7 @@ export class VideoChannelModel extends Model<Partial<AttributesOnly<VideoChannel
|
|||
|
||||
// Avoid error when running this method on MAccount... | MChannel...
|
||||
getClientUrl (this: MAccountHost | MChannelHost) {
|
||||
return WEBSERVER.URL + '/c/' + this.Actor.getIdentifier()
|
||||
return WEBSERVER.URL + '/c/' + this.Actor.getIdentifier() + '/videos'
|
||||
}
|
||||
|
||||
getDisplayName () {
|
||||
|
|
|
@ -9773,10 +9773,10 @@ components:
|
|||
description: P2P peers connected (doesn't include WebSeed peers)
|
||||
resolutionChanges:
|
||||
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:
|
||||
type: number
|
||||
description: How many errors occured since the last metric creation
|
||||
description: How many errors occurred since the last metric creation
|
||||
downloadedBytesP2P:
|
||||
type: number
|
||||
description: How many bytes were downloaded with P2P since the last metric creation
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -185,7 +185,7 @@ peertube-runner list-registered
|
|||
|
||||
## 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.
|
||||
|
||||
### Parse logs
|
||||
|
|
Loading…
Reference in New Issue