diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e1738b17..b9535fa94 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/client/package.json b/client/package.json index 56aa54814..8bdddf804 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "peertube-client", - "version": "6.0.1", + "version": "6.0.2", "private": true, "license": "AGPL-3.0", "author": { diff --git a/client/src/app/+stats/video/video-stats.component.html b/client/src/app/+stats/video/video-stats.component.html index 242a5a7a2..18df800c7 100644 --- a/client/src/app/+stats/video/video-stats.component.html +++ b/client/src/app/+stats/video/video-stats.component.html @@ -45,7 +45,7 @@ -
+
+ plugins: Partial> 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) } diff --git a/client/src/app/+videos/+video-edit/video-update.component.ts b/client/src/app/+videos/+video-edit/video-update.component.ts index 82f45f73d..e2bed35e8 100644 --- a/client/src/app/+videos/+video-edit/video-update.component.ts +++ b/client/src/app/+videos/+video-edit/video-update.component.ts @@ -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) diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.html b/client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.html index f34e9a437..1ebf10b76 100644 --- a/client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.html +++ b/client/src/app/+videos/+video-watch/shared/metadata/video-attributes.component.html @@ -35,7 +35,7 @@
Originally published - {{ video.originallyPublishedAt | date: 'dd MMMM yyyy' }} + {{ video.originallyPublishedAt | date: 'shortDate' }}
diff --git a/client/src/assets/player/shared/player-options-builder/web-video-options-builder.ts b/client/src/assets/player/shared/player-options-builder/web-video-options-builder.ts index 9cf74d8ed..b69ab1d34 100644 --- a/client/src/assets/player/shared/player-options-builder/web-video-options-builder.ts +++ b/client/src/assets/player/shared/player-options-builder/web-video-options-builder.ts @@ -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 || [] } } } diff --git a/client/src/assets/player/shared/web-video/web-video-plugin.ts b/client/src/assets/player/shared/web-video/web-video-plugin.ts index 8f4db0680..0f5015c9a 100644 --- a/client/src/assets/player/shared/web-video/web-video-plugin.ts +++ b/client/src/assets/player/shared/web-video/web-video-plugin.ts @@ -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) diff --git a/package.json b/package.json index 4fddab833..4900edbec 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/packages/core-utils/src/string/chapters.ts b/packages/core-utils/src/string/chapters.ts index d7643665c..2f8105f3d 100644 --- a/packages/core-utils/src/string/chapters.ts +++ b/packages/core-utils/src/string/chapters.ts @@ -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 [] } diff --git a/packages/tests/src/api/videos/video-chapters.ts b/packages/tests/src/api/videos/video-chapters.ts index 2f3dbcd2e..bed1ccef8 100644 --- a/packages/tests/src/api/videos/video-chapters.ts +++ b/packages/tests/src/api/videos/video-chapters.ts @@ -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' } }) diff --git a/packages/tests/src/client/index-html.ts b/packages/tests/src/client/index-html.ts index 9ff8b8957..4cb9a41c5 100644 --- a/packages/tests/src/client/index-html.ts +++ b/packages/tests/src/client/index-html.ts @@ -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(``) + expect(res.text).to.contain(``) } 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(``) + expect(res.text).to.contain(``) } channelURLtests(await makeHTMLRequest(servers[0].url, '/video-channels/root_channel@' + servers[0].host)) diff --git a/packages/tests/src/client/og-twitter-tags.ts b/packages/tests/src/client/og-twitter-tags.ts index 8d7cde990..cdd4a15c7 100644 --- a/packages/tests/src/client/og-twitter-tags.ts +++ b/packages/tests/src/client/og-twitter-tags.ts @@ -48,7 +48,7 @@ describe('Test Open Graph and Twitter cards HTML tags', function () { expect(text).to.contain(``) expect(text).to.contain(``) expect(text).to.contain('') - expect(text).to.contain(``) + expect(text).to.contain(``) } async function channelPageTest (path: string) { @@ -58,7 +58,7 @@ describe('Test Open Graph and Twitter cards HTML tags', function () { expect(text).to.contain(``) expect(text).to.contain(``) expect(text).to.contain('') - expect(text).to.contain(``) + expect(text).to.contain(``) } async function watchVideoPageTest (path: string) { diff --git a/packages/tests/src/misc-endpoints.ts b/packages/tests/src/misc-endpoints.ts index eab3bdde5..95c01061f 100644 --- a/packages/tests/src/misc-endpoints.ts +++ b/packages/tests/src/misc-endpoints.ts @@ -212,11 +212,11 @@ describe('Test misc endpoints', function () { expect(res.text).to.contain('video 2') expect(res.text).to.not.contain('video 3') - expect(res.text).to.contain('' + server.url + '/c/channel1') - expect(res.text).to.contain('' + server.url + '/c/channel2') + expect(res.text).to.contain('' + server.url + '/c/channel1/videos') + expect(res.text).to.contain('' + server.url + '/c/channel2/videos') - expect(res.text).to.contain('' + server.url + '/a/user1') - expect(res.text).to.contain('' + server.url + '/a/user2') + expect(res.text).to.contain('' + server.url + '/a/user1/video-channels') + expect(res.text).to.contain('' + server.url + '/a/user2/video-channels') }) it('Should not fail with big title/description videos', async function () { diff --git a/packages/tests/src/plugins/id-and-pass-auth.ts b/packages/tests/src/plugins/id-and-pass-auth.ts index a332f0eec..9fcdf5aa9 100644 --- a/packages/tests/src/plugins/id-and-pass-auth.ts +++ b/packages/tests/src/plugins/id-and-pass-auth.ts @@ -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 ]) }) diff --git a/packages/tests/src/server-helpers/core-utils.ts b/packages/tests/src/server-helpers/core-utils.ts index d2b5b0512..43afab39a 100644 --- a/packages/tests/src/server-helpers/core-utils.ts +++ b/packages/tests/src/server-helpers/core-utils.ts @@ -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' } + ]) + }) }) diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh index 31eaa1990..49055bdb3 100755 --- a/scripts/upgrade.sh +++ b/scripts/upgrade.sh @@ -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} + diff --git a/server/core/initializers/constants.ts b/server/core/initializers/constants.ts index b8ac151b1..aed84fda0 100644 --- a/server/core/initializers/constants.ts +++ b/server/core/initializers/constants.ts @@ -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: { diff --git a/server/core/lib/auth/oauth-model.ts b/server/core/lib/auth/oauth-model.ts index 26a9f8996..47362931f 100644 --- a/server/core/lib/auth/oauth-model.ts +++ b/server/core/lib/auth/oauth-model.ts @@ -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.') diff --git a/server/core/lib/html/shared/actor-html.ts b/server/core/lib/html/shared/actor-html.ts index 121b22afe..0a9056483 100644 --- a/server/core/lib/html/shared/actor-html.ts +++ b/server/core/lib/html/shared/actor-html.ts @@ -80,6 +80,10 @@ export class ActorHtml { ogType, twitterCard, schemaType, + jsonldProfile: { + createdAt: entity.createdAt, + updatedAt: entity.updatedAt + }, indexationPolicy: entity.Actor.isOwned() ? 'always' diff --git a/server/core/lib/html/shared/tags-html.ts b/server/core/lib/html/shared/tags-html.ts index 297888605..3df840fd9 100644 --- a/server/core/lib/html/shared/tags-html.ts +++ b/server/core/lib/html/shared/tags-html.ts @@ -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, diff --git a/server/core/lib/job-queue/handlers/shared/move-video.ts b/server/core/lib/job-queue/handlers/shared/move-video.ts index e056e9657..9aeb55353 100644 --- a/server/core/lib/job-queue/handlers/shared/move-video.ts +++ b/server/core/lib/job-queue/handlers/shared/move-video.ts @@ -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) } diff --git a/server/core/lib/live/shared/transcoding-wrapper/ffmpeg-transcoding-wrapper.ts b/server/core/lib/live/shared/transcoding-wrapper/ffmpeg-transcoding-wrapper.ts index 464686470..e8fd192ff 100644 --- a/server/core/lib/live/shared/transcoding-wrapper/ffmpeg-transcoding-wrapper.ts +++ b/server/core/lib/live/shared/transcoding-wrapper/ffmpeg-transcoding-wrapper.ts @@ -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') diff --git a/server/core/lib/video-chapters.ts b/server/core/lib/video-chapters.ts index c2b091356..71e2a7cf4 100644 --- a/server/core/lib/video-chapters.ts +++ b/server/core/lib/video-chapters.ts @@ -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 }) diff --git a/server/core/models/account/account.ts b/server/core/models/account/account.ts index 6d1b204e4..daede0abb 100644 --- a/server/core/models/account/account.ts +++ b/server/core/models/account/account.ts @@ -457,7 +457,7 @@ export class AccountModel extends Model>> { // 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 () { diff --git a/server/core/models/user/user.ts b/server/core/models/user/user.ts index 3c4495e3e..84060bc08 100644 --- a/server/core/models/user/user.ts +++ b/server/core/models/user/user.ts @@ -873,6 +873,8 @@ export class UserModel extends Model>> { } isPasswordMatch (password: string) { + if (!password || !this.password) return false + return comparePassword(password, this.password) } diff --git a/server/core/models/video/video-channel.ts b/server/core/models/video/video-channel.ts index 5a6e1b384..9f760f66e 100644 --- a/server/core/models/video/video-channel.ts +++ b/server/core/models/video/video-channel.ts @@ -841,7 +841,7 @@ export class VideoChannelModel extends Model