1
0
Fork 0
-----BEGIN PGP SIGNATURE-----
 
 iQEzBAABCAAdFiEExEqtY4NnkSypPt1XWDphLYkBWb4FAmWo43oACgkQWDphLYkB
 Wb4nNQf+OXtesIZuL0Zk/zMNiLLc3EoNWpT1qPoWr2FZpq+OQQI1UnfuD9J+yU9V
 2SQLPqAlMM2tb4rEtDoIrGYHnb4P8Urb05UtqGPRPEOkZ0qUF+rPzPSYNFBGkapE
 gjQJsOzsQJgRproiRp9EtkquRuOWTArz3My8vRxQ6rZrIWnJNch5pdf+ljA89q5/
 RBJsEdMKo8HQe+RCWSb7whbvjaFCQGBEDN2xtpIsOiHcFJgCEh5r2yWW+YPUgvMH
 VGuwtRlPAA34Qk4r27C/Xov8/WFqJuSSLTtDoeYfHW0/pQRfgnjzORfBw4rpW3WH
 ltiaFNRd2bgkDcFOlez94C29Topf+Q==
 =zWmM
 -----END PGP SIGNATURE-----
gpgsig -----BEGIN PGP SIGNATURE-----
 
 iQJNBAABCAA3FiEEGCBYi9NGimfngx9dVTwOu+tdXwgFAmXHpMAZHGtvdG92YWxl
 eGFyaWFuQGdtYWlsLmNvbQAKCRBVPA67611fCG88D/4h0ryoXLWqpzH4gSV3JA2U
 qj1dRcJp/zLTZTxifsaZ/c9pHjcvGgJauC3KnaPZtY+b9zmtV9ulvHrHJptjV/1q
 yMfepmSfcb3OnHfMqHojVWr6qUk4z0+glgk2Atx/RGdm+fimivaZs/RiuVhuNt1V
 QpttMG/g/y29p+Pwffo9h1izetcEi6dh2tKxrmelzdLC7Kt+jaFHGWH4mhAgKuJG
 K7D59JBRVu0a8FwkQOblBpGGjpwOFCNGHCgiPGy1CGExsXjZxwhB8DzrkpgbO/lE
 Ya/Cdr68TzlNY7FDZeucwKQjrsfPPuoRPY0H/5XdFDRR7NpuA4DfqAk+1oXNiPAa
 9Ua5J8iQdkGRQ1bkCJGglcPCgVUT9fGYHBCoi+nz3rwiLpbd9NB+Cl/9zeg27A36
 PmVJZXCP4UiE+blnUiJD3yaRP5wpiOMiud8tACzbQvOTxu3nhd5drQpAWqtC4sF5
 F+7ZHqieKHJYSTMSILuxXtgDADYDKDLiZ6oz6TYwQSu37WloBJkkhnMAXXF8BGBt
 J7XrYOej6/kM5Q20ACy9TLIP9R585yaUXR9d4ZhMePA+2p6As9xdh1hUvxa17pZL
 3ltPnflqLLO+HnLR+r3QJ1cLzwpb6r8iB0IEhu76Akrse37QqNnFYB4rWpIH3eeF
 iLEZMLaqYiNINJvJCnoTww==
 =+Hun
 -----END PGP SIGNATURE-----

Merge tag 'v6.0.3' into changes

v6.0.3
This commit is contained in:
Alex Kotov 2024-02-10 20:30:55 +04:00
commit 8b561d08cf
45 changed files with 416 additions and 100 deletions

View File

@ -1,5 +1,36 @@
# Changelog
## v6.0.3
### IMPORTANT NOTES
* If you upgrade from PeerTube **< v6.0.0**, please follow v6.0.0 IMPORTANT NOTES
* If you upgrade from PeerTube **v6.0.0**, please follow v6.0.1 IMPORTANT NOTES
### SECURITY
* Prevent nginx from serving private/internal/password protected HLS video static files
* You must update your nginx configuration like in [this commit](https://github.com/Chocobozzz/PeerTube/commit/12ea8f0dd11e3fb5fbb8955f5b7d52f27332d619#diff-be9f96b9b1de67284047e610821493f9a5bec86bfcdf81a7d8d6e7904474c186) (line `202` replace `location ~ ^(/static/(webseed|web-videos|streaming-playlists)/private/)|^/download {` by `location ~ ^(/static/(webseed|web-videos|streaming-playlists/hls)/private/)|^/download {`)
### Bug fixes
* Fix HTML meta tags with attributes that contain quotes
* Fix time parsing resulting in broken video start time in some cases
* Fix WebTorrent video import crash
* Reload *Discover* page on logout
* Fix privacy error when updating a live, even if the privacy has not changed
* Fix invalid remote live state change notification that causes the player to reload
* Don't apply big play button skin to settings menu
* Fix downloading video files from object storage with some video names (that include emojis, quotes etc)
* Fix thumbnail generation when ffmpeg cannot seek the input
* Fix theme colors on stats page
* Fix input mask (used for chapters, playlist timecodes...) with 10h+ videos
* Fix chapter *position* width consistency
* Fix player ratio with audio only videos
* Also update video playlist URLs when using `update-host` script
* Fix upload/import/update of videos that contain multiple chapters with the same timecode
## v6.0.2
### IMPORTANT NOTES

View File

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

View File

@ -1,4 +1,4 @@
import { ChartConfiguration, ChartData, ChartOptions, PluginOptionsByType, Scale, TooltipItem } from 'chart.js'
import { ChartConfiguration, ChartData, ChartOptions, PluginOptionsByType, Scale, TooltipItem, defaults as ChartJSDefaults } from 'chart.js'
import zoomPlugin from 'chartjs-plugin-zoom'
import { Observable, of } from 'rxjs'
import { SelectOptionsItem } from 'src/types'
@ -35,6 +35,10 @@ type ChartBuilderResult = {
type Card = { label: string, value: string | number, moreInfo?: string, help?: string }
ChartJSDefaults.backgroundColor = getComputedStyle(document.body).getPropertyValue('--mainBackgroundColor')
ChartJSDefaults.borderColor = getComputedStyle(document.body).getPropertyValue('--greySecondaryBackgroundColor')
ChartJSDefaults.color = getComputedStyle(document.body).getPropertyValue('--mainForegroundColor')
@Component({
templateUrl: './video-stats.component.html',
styleUrls: [ './video-stats.component.scss' ],

View File

@ -130,6 +130,7 @@ p-calendar {
.position {
height: 31px;
min-width: 16px;
display: flex;
align-items: center;
}

View File

@ -1,5 +1,5 @@
import { Subject } from 'rxjs'
import { Component, OnInit } from '@angular/core'
import { Subject, Subscription, switchMap } from 'rxjs'
import { Component, OnDestroy, OnInit } from '@angular/core'
import { Notifier, ScreenService, User, UserService } from '@app/core'
import { Video } from '@app/shared/shared-main'
import { OverviewService } from './overview.service'
@ -10,7 +10,7 @@ import { VideosOverview } from './videos-overview.model'
templateUrl: './video-overview.component.html',
styleUrls: [ './video-overview.component.scss' ]
})
export class VideoOverviewComponent implements OnInit {
export class VideoOverviewComponent implements OnInit, OnDestroy {
onDataSubject = new Subject<any>()
overviews: VideosOverview[] = []
@ -24,6 +24,8 @@ export class VideoOverviewComponent implements OnInit {
private lastWasEmpty = false
private isLoading = false
private userSub: Subscription
constructor (
private notifier: Notifier,
private userService: UserService,
@ -37,8 +39,18 @@ export class VideoOverviewComponent implements OnInit {
this.userService.getAnonymousOrLoggedUser()
.subscribe(user => this.userMiniature = user)
this.userService.listenAnonymousUpdate()
.subscribe(user => this.userMiniature = user)
this.userSub = this.userService.listenAnonymousUpdate()
.pipe(switchMap(() => this.userService.getAnonymousOrLoggedUser()))
.subscribe(user => {
this.userMiniature = user
this.overviews = []
this.loadMoreResults()
})
}
ngOnDestroy () {
if (this.userSub) this.userSub.unsubscribe()
}
buildVideoChannelBy (object: { videos: Video[] }) {

View File

@ -1,5 +1,5 @@
<p-inputMask
[disabled]="disabled" [(ngModel)]="timestampString" (onBlur)="onBlur()"
[ngClass]="{ 'border-disabled': disableBorder }"
mask="9:99:99" slotChar="0" (ngModelChange)="onModelChange()" [inputId]="inputName"
mask="99:99:99" slotChar="0" (ngModelChange)="onModelChange()" [inputId]="inputName"
></p-inputMask>

View File

@ -26,6 +26,8 @@ p-inputmask {
&:not(.border-disabled) {
::ng-deep input {
@include peertube-input-text(80px);
padding: 3px 10px;
}
}
}

View File

@ -36,7 +36,8 @@ export class TimestampInputComponent implements ControlValueAccessor, OnInit {
writeValue (timestamp: number) {
this.timestamp = timestamp
this.timestampString = secondsToTime(this.timestamp, true, ':')
this.timestampString = secondsToTime({ seconds: this.timestamp, fullFormat: true, symbol: ':' })
console.log(this.timestampString)
}
registerOnChange (fn: (_: any) => void) {

View File

@ -144,8 +144,8 @@ export class VideoPlaylistElementMiniatureComponent implements OnInit {
const start = playlistElement.startTimestamp
const stop = playlistElement.stopTimestamp
const startFormatted = secondsToTime(start, true, ':')
const stopFormatted = secondsToTime(stop, true, ':')
const startFormatted = secondsToTime({ seconds: start, fullFormat: true, symbol: ':' })
const stopFormatted = secondsToTime({ seconds: stop, fullFormat: true, symbol: ':' })
if (start === null && stop === null) return ''

View File

@ -38,6 +38,8 @@ class PeerTubePlugin extends Plugin {
private errorModal: videojs.ModalDialog
private hasInitialSeek = false
private videoViewOnPlayHandler: (...args: any[]) => void
private videoViewOnSeekedHandler: (...args: any[]) => void
private videoViewOnEndedHandler: (...args: any[]) => void
@ -227,7 +229,7 @@ class PeerTubePlugin extends Plugin {
// In portrait screen mode, we allow player with bigger height size than width
const portraitMode = getComputedStyle(el).getPropertyValue(this.options.autoPlayerRatio.cssPlayerPortraitModeVariable) === '1'
const currentRatio = !portraitMode && data.ratio < 1
const currentRatio = isNaN(data.ratio) || (!portraitMode && data.ratio < 1)
? defaultRatio
: data.ratio
@ -242,6 +244,7 @@ class PeerTubePlugin extends Plugin {
let lastCurrentTime = startTime
let lastViewEvent: VideoViewEvent
let ended = false // player.ended() is too "slow", so handle ended manually
if (this.videoViewInterval) clearInterval(this.videoViewInterval)
if (this.videoViewOnPlayHandler) this.player.off('play', this.videoViewOnPlayHandler)
@ -249,22 +252,38 @@ class PeerTubePlugin extends Plugin {
if (this.videoViewOnEndedHandler) this.player.off('ended', this.videoViewOnEndedHandler)
this.videoViewOnPlayHandler = () => {
debugLogger('Notify user is watching on play: ' + startTime)
this.notifyUserIsWatching(startTime, lastViewEvent)
}
this.videoViewOnSeekedHandler = () => {
// Bypass the first initial seek
if (this.hasInitialSeek) {
this.hasInitialSeek = false
return
}
const diff = Math.floor(this.player.currentTime()) - lastCurrentTime
// Don't take into account small forwards
if (diff > 0 && diff < 3) return
debugLogger('Detected seek event for user watching')
lastViewEvent = 'seek'
}
this.videoViewOnEndedHandler = () => {
ended = true
if (this.options.isLive()) return
const currentTime = Math.floor(this.player.duration())
lastCurrentTime = currentTime
debugLogger('Notify user is watching on end: ' + currentTime)
this.notifyUserIsWatching(currentTime, lastViewEvent)
lastViewEvent = undefined
@ -275,11 +294,15 @@ class PeerTubePlugin extends Plugin {
this.player.one('ended', this.videoViewOnEndedHandler)
this.videoViewInterval = setInterval(() => {
if (ended) return
const currentTime = Math.floor(this.player.currentTime())
// No need to update
if (currentTime === lastCurrentTime) return
debugLogger('Notify user is watching: ' + currentTime)
lastCurrentTime = currentTime
this.notifyUserIsWatching(currentTime, lastViewEvent)
@ -418,6 +441,7 @@ class PeerTubePlugin extends Plugin {
if (startTime !== null && startTime !== undefined) {
debugLogger('Start the video at ' + startTime)
this.hasInitialSeek = true
this.player.currentTime(timeToInt(startTime))
}

View File

@ -33,6 +33,8 @@
body {
--bs-border-color-translucent: #{pvar(--inputBorderColor)};
--bs-body-color: #{pvar(--mainForegroundColor)};
}
.accordion {

View File

@ -100,12 +100,15 @@ body {
}
}
.vjs-control-bar,
.vjs-big-play-button,
.vjs-settings-dialog {
.vjs-big-play-button {
background-color: pvar(--embedBigPlayBackgroundColor);
}
.vjs-settings-dialog,
.vjs-control-bar {
background-color: $primary-background-color;
}
.vjs-poster {
outline: 0;
background-size: cover;

View File

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

View File

@ -52,13 +52,13 @@ function timeToInt (time: number | string) {
if (typeof time === 'number') return time
// Try with 00h00m00s format first
const reg = new RegExp(`^(\\d+h)?(\\d+m)?(\\d+)s?$`)
const reg = new RegExp(`^((?<hours>\\d+)h)?((?<minutes>\\d+)m)?((?<seconds>\\d+)s?)?$`)
const matches = time.match(reg)
if (matches) {
const hours = parseInt(matches[1] || '0', 10)
const minutes = parseInt(matches[2] || '0', 10)
const seconds = parseInt(matches[3] || '0', 10)
const hours = parseInt(matches.groups['hours'] || '0', 10)
const minutes = parseInt(matches.groups['minutes'] || '0', 10)
const seconds = parseInt(matches.groups['seconds'] || '0', 10)
return hours * 3600 + minutes * 60 + seconds
}
@ -83,29 +83,46 @@ function timeToInt (time: number | string) {
return result
}
function secondsToTime (seconds: number, full = false, symbol?: string) {
function secondsToTime (options: {
seconds: number
fullFormat?: boolean // default false
symbol?: string
} | number) {
let seconds: number
let fullFormat = false
let symbol: string
if (typeof options === 'number') {
seconds = options
} else {
seconds = options.seconds
fullFormat = options.fullFormat ?? false
symbol = options.symbol
}
let time = ''
if (seconds === 0 && !full) return '0s'
if (seconds === 0 && !fullFormat) return '0s'
const hourSymbol = (symbol || 'h')
const minuteSymbol = (symbol || 'm')
const secondsSymbol = full ? '' : 's'
const secondsSymbol = fullFormat ? '' : 's'
const hours = Math.floor(seconds / 3600)
if (hours >= 1) time = hours + hourSymbol
else if (full) time = '0' + hourSymbol
if (hours >= 1 && hours < 10 && fullFormat) time = '0' + hours + hourSymbol
else if (hours >= 1) time = hours + hourSymbol
else if (fullFormat) time = '00' + hourSymbol
seconds %= 3600
const minutes = Math.floor(seconds / 60)
if (minutes >= 1 && minutes < 10 && full) time += '0' + minutes + minuteSymbol
if (minutes >= 1 && minutes < 10 && fullFormat) time += '0' + minutes + minuteSymbol
else if (minutes >= 1) time += minutes + minuteSymbol
else if (full) time += '00' + minuteSymbol
else if (fullFormat) time += '00' + minuteSymbol
seconds %= 60
if (seconds >= 1 && seconds < 10 && full) time += '0' + seconds + secondsSymbol
if (seconds >= 1 && seconds < 10 && fullFormat) time += '0' + seconds + secondsSymbol
else if (seconds >= 1) time += seconds + secondsSymbol
else if (full) time += '00'
else if (fullFormat) time += '00'
return time
}

View File

@ -69,3 +69,9 @@ export function escapeHTML (stringParam: string) {
return String(stringParam).replace(/[&<>"'`=/]/g, s => entityMap[s])
}
export function escapeAttribute (value: string) {
if (!value) return ''
return String(value).replace(/"/g, '\\"')
}

View File

@ -79,6 +79,10 @@ export class FFmpegCommandWrapper {
// ---------------------------------------------------------------------------
resetCommand () {
this.command = undefined
}
buildCommand (input: string) {
if (this.command) throw new Error('Command is already built')

View File

@ -36,26 +36,51 @@ export class FFmpegImage {
return this.commandWrapper.runCommand()
}
// ---------------------------------------------------------------------------
async generateThumbnailFromVideo (options: {
fromPath: string
output: string
framesToAnalyze: number
ffprobe?: FfprobeData
}) {
const { fromPath, output, ffprobe, framesToAnalyze } = options
const { fromPath, ffprobe } = options
let duration = await getVideoStreamDuration(fromPath, ffprobe)
if (isNaN(duration)) duration = 0
this.commandWrapper.buildCommand(fromPath)
this.buildGenerateThumbnailFromVideo(options)
.seekInput(duration / 2)
try {
return await this.commandWrapper.runCommand()
} catch (err) {
this.commandWrapper.debugLog('Cannot generate thumbnail from video using seek input, fallback to no seek', { err })
this.commandWrapper.resetCommand()
this.buildGenerateThumbnailFromVideo(options)
return this.commandWrapper.runCommand()
}
}
private buildGenerateThumbnailFromVideo (options: {
fromPath: string
output: string
framesToAnalyze: number
}) {
const { fromPath, output, framesToAnalyze } = options
return this.commandWrapper.buildCommand(fromPath)
.videoFilter('thumbnail=' + framesToAnalyze)
.outputOption('-frames:v 1')
.outputOption('-abort_on empty_output')
.output(output)
return this.commandWrapper.runCommand()
}
// ---------------------------------------------------------------------------
async generateStoryboardFromVideo (options: {
path: string
destination: string

View File

@ -269,7 +269,7 @@ export type NotifyPayload =
export interface FederateVideoPayload {
videoUUID: string
isNewVideo: boolean
isNewVideoForFederation: boolean
}
// ---------------------------------------------------------------------------

View File

@ -25,7 +25,7 @@ export class PlaylistsCommand extends AbstractCommand {
count?: number
sort?: string
playlistType?: VideoPlaylistType_Type
}) {
} = {}) {
const path = '/api/v1/video-playlists'
const query = pick(options, [ 'start', 'count', 'sort', 'playlistType' ])

View File

@ -66,7 +66,7 @@ describe('Save replay setting', function () {
const liveDetails = await servers[0].videos.get({ id: liveVideoUUID })
await waitJobs(servers)
await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200)
await checkVideosExist(liveVideoUUID, null, HttpStatusCode.OK_200)
return { ffmpegCommand, liveDetails }
}
@ -105,13 +105,14 @@ describe('Save replay setting', function () {
return { liveDetails }
}
async function checkVideosExist (videoId: string, existsInList: boolean, expectedStatus?: HttpStatusCodeType) {
async function checkVideosExist (videoId: string, videosLength: number, expectedStatus?: HttpStatusCodeType) {
for (const server of servers) {
const length = existsInList ? 1 : 0
const { data, total } = await server.videos.list()
expect(data).to.have.lengthOf(length)
expect(total).to.equal(length)
if (videosLength !== null) {
expect(data).to.have.lengthOf(videosLength)
expect(total).to.equal(videosLength)
}
if (expectedStatus) {
await server.videos.get({ id: videoId, expectedStatus })
@ -172,7 +173,7 @@ describe('Save replay setting', function () {
await waitJobs(servers)
await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200)
await checkVideosExist(liveVideoUUID, 0, HttpStatusCode.OK_200)
await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE)
})
@ -187,7 +188,7 @@ describe('Save replay setting', function () {
await waitJobs(servers)
await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200)
await checkVideosExist(liveVideoUUID, 1, HttpStatusCode.OK_200)
await checkVideoState(liveVideoUUID, VideoState.PUBLISHED)
})
@ -203,7 +204,7 @@ describe('Save replay setting', function () {
await waitJobs(servers)
// Live still exist, but cannot be played anymore
await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200)
await checkVideosExist(liveVideoUUID, 0, HttpStatusCode.OK_200)
await checkVideoState(liveVideoUUID, VideoState.LIVE_ENDED)
// No resolutions saved since we did not save replay
@ -234,7 +235,7 @@ describe('Save replay setting', function () {
await publishLiveAndBlacklist({ permanent: false, replay: false })
await checkVideosExist(liveVideoUUID, false)
await checkVideosExist(liveVideoUUID, 0)
await servers[0].videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
await servers[1].videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
@ -258,7 +259,7 @@ describe('Save replay setting', function () {
await publishLiveAndDelete({ permanent: false, replay: false })
await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404)
await checkVideosExist(liveVideoUUID, 0, HttpStatusCode.NOT_FOUND_404)
await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false })
})
})
@ -272,7 +273,7 @@ describe('Save replay setting', function () {
await waitJobs(servers)
await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200)
await checkVideosExist(liveVideoUUID, 0, HttpStatusCode.OK_200)
await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE)
await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC)
})
@ -285,7 +286,7 @@ describe('Save replay setting', function () {
await waitJobs(servers)
await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200)
await checkVideosExist(liveVideoUUID, 1, HttpStatusCode.OK_200)
await checkVideoState(liveVideoUUID, VideoState.PUBLISHED)
await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC)
})
@ -306,7 +307,7 @@ describe('Save replay setting', function () {
await waitJobs(servers)
// Live has been transcoded
await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200)
await checkVideosExist(liveVideoUUID, 0, HttpStatusCode.OK_200)
await checkVideoState(liveVideoUUID, VideoState.PUBLISHED)
await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.UNLISTED)
})
@ -354,7 +355,7 @@ describe('Save replay setting', function () {
await publishLiveAndBlacklist({ permanent: false, replay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } })
await checkVideosExist(liveVideoUUID, false)
await checkVideosExist(liveVideoUUID, 0)
await servers[0].videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
await servers[1].videos.get({ id: liveVideoUUID, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
@ -369,7 +370,7 @@ describe('Save replay setting', function () {
await publishLiveAndDelete({ permanent: false, replay: true, replaySettings: { privacy: VideoPrivacy.PUBLIC } })
await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404)
await checkVideosExist(liveVideoUUID, 0, HttpStatusCode.NOT_FOUND_404)
await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false })
})
})
@ -379,6 +380,19 @@ describe('Save replay setting', function () {
describe('With a first live and its replay', function () {
before(async function () {
this.timeout(120000)
await servers[0].kill()
await servers[0].run({
federation: {
videos: {
federate_unlisted: false
}
}
})
})
it('Should correctly create and federate the "waiting for stream" live', async function () {
this.timeout(120000)
@ -386,7 +400,7 @@ describe('Save replay setting', function () {
await waitJobs(servers)
await checkVideosExist(liveVideoUUID, false, HttpStatusCode.OK_200)
await checkVideosExist(liveVideoUUID, 0, HttpStatusCode.OK_200)
await checkVideoState(liveVideoUUID, VideoState.WAITING_FOR_LIVE)
await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC)
})
@ -399,12 +413,12 @@ describe('Save replay setting', function () {
await waitJobs(servers)
await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200)
await checkVideosExist(liveVideoUUID, 1, HttpStatusCode.OK_200)
await checkVideoState(liveVideoUUID, VideoState.PUBLISHED)
await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC)
})
it('Should correctly have saved the live and federated it after the streaming', async function () {
it('Should correctly have saved the live', async function () {
this.timeout(120000)
const liveDetails = await servers[0].videos.get({ id: liveVideoUUID })
@ -417,13 +431,21 @@ describe('Save replay setting', function () {
const video = await findExternalSavedVideo(servers[0], liveDetails)
expect(video).to.exist
for (const server of servers) {
await server.videos.get({ id: video.uuid })
}
await servers[0].videos.get({ id: video.uuid })
await servers[1].videos.get({ id: video.uuid, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
lastReplayUUID = video.uuid
})
it('Should federate the replay after updating its privacy to public', async function () {
this.timeout(120000)
await servers[0].videos.update({ id: lastReplayUUID, attributes: { privacy: VideoPrivacy.PUBLIC } })
await waitJobs(servers)
await servers[1].videos.get({ id: lastReplayUUID, expectedStatus: HttpStatusCode.OK_200 })
})
it('Should have appropriate ended session and replay live session', async function () {
const { data, total } = await servers[0].live.listSessions({ videoId: liveVideoUUID })
expect(total).to.equal(1)
@ -449,9 +471,9 @@ describe('Save replay setting', function () {
})
it('Should have the first live replay with correct settings', async function () {
await checkVideosExist(lastReplayUUID, false, HttpStatusCode.OK_200)
await checkVideosExist(lastReplayUUID, 1, HttpStatusCode.OK_200)
await checkVideoState(lastReplayUUID, VideoState.PUBLISHED)
await checkVideoPrivacy(lastReplayUUID, VideoPrivacy.UNLISTED)
await checkVideoPrivacy(lastReplayUUID, VideoPrivacy.PUBLIC)
})
})
@ -477,7 +499,7 @@ describe('Save replay setting', function () {
await waitJobs(servers)
await checkVideosExist(liveVideoUUID, true, HttpStatusCode.OK_200)
await checkVideosExist(liveVideoUUID, 2, HttpStatusCode.OK_200)
await checkVideoState(liveVideoUUID, VideoState.PUBLISHED)
await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.PUBLIC)
})
@ -527,7 +549,7 @@ describe('Save replay setting', function () {
})
it('Should have the first live replay with correct settings', async function () {
await checkVideosExist(lastReplayUUID, true, HttpStatusCode.OK_200)
await checkVideosExist(lastReplayUUID, 2, HttpStatusCode.OK_200)
await checkVideoState(lastReplayUUID, VideoState.PUBLISHED)
await checkVideoPrivacy(lastReplayUUID, VideoPrivacy.PUBLIC)
})
@ -550,7 +572,7 @@ describe('Save replay setting', function () {
expect(replay).to.exist
for (const videoId of [ liveVideoUUID, replay.uuid ]) {
await checkVideosExist(videoId, false)
await checkVideosExist(videoId, 1)
await servers[0].videos.get({ id: videoId, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
await servers[1].videos.get({ id: videoId, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
@ -571,7 +593,7 @@ describe('Save replay setting', function () {
const replay = await findExternalSavedVideo(servers[0], liveDetails)
expect(replay).to.not.exist
await checkVideosExist(liveVideoUUID, false, HttpStatusCode.NOT_FOUND_404)
await checkVideosExist(liveVideoUUID, 1, HttpStatusCode.NOT_FOUND_404)
await checkLiveCleanup({ server: servers[0], videoUUID: liveVideoUUID, permanent: false })
})
})

View File

@ -86,9 +86,13 @@ describe('Test live socket messages', function () {
await waitUntilLivePublishedOnAllServers(servers, liveVideoUUID)
await waitJobs(servers)
// Ensure remote server doesn't send multiple times the state change event to viewers
await servers[0].videos.update({ id: liveVideoUUID, attributes: { name: 'my new live name' } })
await waitJobs(servers)
for (const stateChanges of [ localStateChanges, remoteStateChanges ]) {
expect(stateChanges).to.have.length.at.least(1)
expect(stateChanges[stateChanges.length - 1]).to.equal(VideoState.PUBLISHED)
expect(stateChanges).to.have.lengthOf(1)
expect(stateChanges[0]).to.equal(VideoState.PUBLISHED)
}
await stopFfmpeg(ffmpegCommand)

View File

@ -9,9 +9,11 @@ import {
makeActivityPubGetRequest,
PeerTubeServer,
setAccessTokensToServers,
setDefaultVideoChannel,
waitJobs
} from '@peertube/peertube-server-commands'
import { parseTorrentVideo } from '@tests/shared/webtorrent.js'
import { VideoPlaylistPrivacy } from '@peertube/peertube-models'
describe('Test update host scripts', function () {
let server: PeerTubeServer
@ -27,6 +29,7 @@ describe('Test update host scripts', function () {
// Run server 2 to have transcoding enabled
server = await createSingleServer(2, overrideConfig)
await setAccessTokensToServers([ server ])
await setDefaultVideoChannel([ server ])
// Upload two videos for our needs
const { uuid: video1UUID } = await server.videos.upload()
@ -47,6 +50,13 @@ describe('Test update host scripts', function () {
const text = 'my super first comment'
await server.comments.createThread({ videoId: video1UUID, text })
// Playlist
{
const attributes = { displayName: 'playlist', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: server.store.channel.id }
const playlist = await server.playlists.create({ attributes })
await server.playlists.addElement({ playlistId: playlist.id, attributes: { videoId: video1UUID } })
}
await waitJobs(server)
})
@ -100,6 +110,23 @@ describe('Test update host scripts', function () {
}
})
it('Should have updated playlist url', async function () {
const body = await server.playlists.list()
expect(body.total).to.equal(1)
for (const playlist of body.data) {
const { body } = await makeActivityPubGetRequest(server.url, '/video-playlists/' + playlist.uuid)
expect(body.id).to.equal('http://127.0.0.1:9002/video-playlists/' + playlist.uuid)
const { data: elements } = await server.playlists.listVideos({ playlistId: playlist.id })
for (const element of elements) {
const { body } = await makeActivityPubGetRequest(server.url, `/video-playlists/${playlist.uuid}/videos/${element.id}`)
expect(body.id).to.equal(`http://127.0.0.1:9002/video-playlists/${playlist.uuid}/videos/${element.id}`)
}
}
})
it('Should have updated torrent hosts', async function () {
this.timeout(30000)

View File

@ -265,6 +265,19 @@ describe('Test Open Graph and Twitter cards HTML tags', function () {
})
})
describe('Escaping', function () {
it('Should correctly escape values', async function () {
await servers[0].users.updateMe({ description: '<strong>"super description"</strong>' })
const res = await makeGetRequest({ url: servers[0].url, path: '/a/root', accept: 'text/html', expectedStatus: HttpStatusCode.OK_200 })
const text = res.text
expect(text).to.contain(`<meta property="twitter:description" content="\\"super description\\"" />`)
expect(text).to.contain(`<meta property="og:description" content="\\"super description\\"" />`)
})
})
after(async function () {
await cleanupTests(servers)
})

View File

@ -0,0 +1,42 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { HttpStatusCode } from '@peertube/peertube-models'
import {
makeRawRequest
} from '@peertube/peertube-server-commands'
describe('Test nginx', function () {
it('Should serve public HLS/web video files', async function () {
const urls = [
// eslint-disable-next-line max-len
'https://peertube2.cpy.re/static/streaming-playlists/hls/85c8e811-3eb7-4823-8dc5-3c268b6dad60/efad77e7-805d-4b20-8bc9-6e99cee38b20-240-fragmented.mp4',
// eslint-disable-next-line max-len
'https://peertube2.cpy.re/static/streaming-playlists/hls/85c8e811-3eb7-4823-8dc5-3c268b6dad60/1afbabfa-5f16-452e-8165-fe9a9a21cdb2-master.m3u8',
// eslint-disable-next-line max-len
'https://peertube2.cpy.re/static/streaming-playlists/hls/85c8e811-3eb7-4823-8dc5-3c268b6dad60/efad77e7-805d-4b20-8bc9-6e99cee38b20-240.m3u8'
]
for (const url of urls) {
await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 })
}
})
it('Should not serve private HLS/web video files', async function () {
const urls = [
// eslint-disable-next-line max-len
'https://peertube2.cpy.re/static/streaming-playlists/hls/private/72f0e8ee-84b9-44b1-9202-3e72ee7f1b65/531f27fe-bb86-42ed-9cf1-eb5bffc4a609-master.m3u8',
// eslint-disable-next-line max-len
'https://peertube2.cpy.re/static/streaming-playlists/hls/private/72f0e8ee-84b9-44b1-9202-3e72ee7f1b65/057dbf01-0557-414c-a546-a1cc82ac5d99-480.m3u8',
// eslint-disable-next-line max-len
'https://peertube2.cpy.re/static/streaming-playlists/hls/private/72f0e8ee-84b9-44b1-9202-3e72ee7f1b65/c9ef3aa7-5ab6-41c5-91c2-058c50a70c3c-segments-sha256.json',
// eslint-disable-next-line max-len
'https://peertube2.cpy.re/static/streaming-playlists/hls/private/72f0e8ee-84b9-44b1-9202-3e72ee7f1b65/057dbf01-0557-414c-a546-a1cc82ac5d99-480-fragmented.mp4',
'https://peertube2.cpy.re/static/web-videos/private/72f0e8ee-84b9-44b1-9202-3e72ee7f1b65-480.mp4'
]
for (const url of urls) {
await makeRawRequest({ url, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
}
})
})

View File

@ -78,6 +78,10 @@ describe('Time to int', function () {
expect(timeToInt('02h02m03s')).to.equal(7323)
expect(timeToInt('2:02:3')).to.equal(7323)
expect(timeToInt('5h10m')).to.equal(5 * 3600 + 60 * 10)
expect(timeToInt('5h10m0s')).to.equal(5 * 3600 + 60 * 10)
expect(timeToInt('5h10m0')).to.equal(5 * 3600 + 60 * 10)
expect(timeToInt(3500)).to.equal(3500)
})
})

View File

@ -59,7 +59,7 @@ elif [ "$1" = "client" ]; then
feedsFiles=$(findTestFiles ./packages/tests/dist/feeds)
clientFiles=$(findTestFiles ./packages/tests/dist/client)
miscFiles="./packages/tests/dist/misc-endpoints.js"
miscFiles="./packages/tests/dist/misc-endpoints.js ./packages/tests/dist/nginx.js"
# Not in their own task, they need an index.html
pluginFiles="./packages/tests/dist/plugins/html-injection.js ./packages/tests/dist/api/server/plugins.js"

View File

@ -2,6 +2,8 @@
set -eu
npm run build:server
npm run concurrently -- -k \
"cd client && npm run webpack -- --config webpack/webpack.video-embed.js --mode development --watch" \
"npm run build:server && NODE_ENV=dev npm start"
"NODE_ENV=dev npm start"

View File

@ -165,7 +165,7 @@ async function addVideoJobsAfterUpload (video: MVideoFullLight, videoFile: MVide
type: 'federate-video' as 'federate-video',
payload: {
videoUUID: video.uuid,
isNewVideo: false
isNewVideoForFederation: false
}
}
]

View File

@ -64,7 +64,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
const videoFileLockReleaser = await VideoPathManager.Instance.lockFiles(videoFromReq.uuid)
try {
const { videoInstanceUpdated, isNewVideo } = await sequelizeTypescript.transaction(async t => {
const { videoInstanceUpdated, isNewVideoForFederation } = await sequelizeTypescript.transaction(async t => {
// Refresh video since thumbnails to prevent concurrent updates
const video = await VideoModel.loadFull(videoFromReq.id, t)
@ -96,9 +96,15 @@ async function updateVideo (req: express.Request, res: express.Response) {
}
// Privacy update?
let isNewVideo = false
let isNewVideoForFederation = false
if (videoInfoToUpdate.privacy !== undefined) {
isNewVideo = await updateVideoPrivacy({ videoInstance: video, videoInfoToUpdate, hadPrivacyForFederation, transaction: t })
isNewVideoForFederation = await updateVideoPrivacy({
videoInstance: video,
videoInfoToUpdate,
hadPrivacyForFederation,
transaction: t
})
}
// Force updatedAt attribute change
@ -155,7 +161,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
)
logger.info('Video with name %s and uuid %s updated.', video.name, video.uuid, lTags(video.uuid))
return { videoInstanceUpdated, isNewVideo }
return { videoInstanceUpdated, isNewVideoForFederation }
})
Hooks.runAction('action:api.video.updated', { video: videoInstanceUpdated, body: req.body, req, res })
@ -164,7 +170,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
video: videoInstanceUpdated,
nameChanged: !!videoInfoToUpdate.name,
oldPrivacy,
isNewVideo
isNewVideoForFederation
})
} catch (err) {
// If the transaction is retried, sequelize will think the object has not changed
@ -181,6 +187,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
.end()
}
// Return a boolean indicating if the video is considered as "new" for remote instances in the federation
async function updateVideoPrivacy (options: {
videoInstance: MVideoFullLight
videoInfoToUpdate: VideoUpdate
@ -188,7 +195,7 @@ async function updateVideoPrivacy (options: {
transaction: Transaction
}) {
const { videoInstance, videoInfoToUpdate, hadPrivacyForFederation, transaction } = options
const isNewVideo = videoInstance.isNewVideo(videoInfoToUpdate.privacy)
const isNewVideoForFederation = videoInstance.isNewVideoForFederation(videoInfoToUpdate.privacy)
const newPrivacy = forceNumber(videoInfoToUpdate.privacy) as VideoPrivacyType
setVideoPrivacy(videoInstance, newPrivacy)
@ -208,7 +215,7 @@ async function updateVideoPrivacy (options: {
await VideoModel.sendDelete(videoInstance, { transaction })
}
return isNewVideo
return isNewVideoForFederation
}
function updateSchedule (videoInstance: MVideoFullLight, videoInfoToUpdate: VideoUpdate, transaction: Transaction) {

View File

@ -269,7 +269,7 @@ async function addVideoJobsAfterUpload (video: MVideoFullLight, videoFile: MVide
type: 'federate-video' as 'federate-video',
payload: {
videoUUID: video.uuid,
isNewVideo: true
isNewVideoForFederation: true
}
}
]

View File

@ -36,7 +36,10 @@ async function downloadWebTorrentVideo (target: { uri: string, torrentName?: str
await ensureDir(directoryPath)
// eslint-disable-next-line new-cap
const webtorrent = new (await import('webtorrent')).default()
const webtorrent = new (await import('webtorrent')).default({
natUpnp: false,
natPmp: false
} as any)
return new Promise<string>((res, rej) => {
let file: TorrentFile

View File

@ -53,6 +53,7 @@ export class APVideoUpdater extends APVideoAbstractBuilder {
this.checkChannelUpdateOrThrow(channelActor)
const oldState = this.video.state
const videoUpdated = await this.updateVideo(channelActor.VideoChannel, undefined, overrideTo)
if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel)
@ -95,7 +96,7 @@ export class APVideoUpdater extends APVideoAbstractBuilder {
Notifier.Instance.notifyOnNewVideoIfNeeded(videoUpdated)
}
if (videoUpdated.isLive) {
if (videoUpdated.isLive && oldState !== videoUpdated.state) {
PeerTubeSocket.Instance.sendVideoLiveNewState(videoUpdated)
}

View File

@ -1,4 +1,4 @@
import { escapeHTML } from '@peertube/peertube-core-utils'
import { escapeAttribute, escapeHTML } from '@peertube/peertube-core-utils'
import { CONFIG } from '../../../initializers/config.js'
import { CUSTOM_HTML_TAG_COMMENTS, EMBED_SIZE, WEBSERVER } from '../../../initializers/constants.js'
import { MVideo, MVideoPlaylist } from '../../../types/models/index.js'
@ -61,7 +61,7 @@ export class TagsHtml {
static addDescriptionTag (htmlStringPage: string, escapedTruncatedDescription?: string) {
const content = escapedTruncatedDescription || escapeHTML(CONFIG.INSTANCE.SHORT_DESCRIPTION)
const descriptionTag = `<meta name="description" content="${content}" />`
const descriptionTag = `<meta name="description" content="${escapeAttribute(content)}" />`
return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.DESCRIPTION, descriptionTag)
}
@ -93,7 +93,7 @@ export class TagsHtml {
const tagValue = openGraphMetaTags[tagName]
if (!tagValue) return
tagsStr += `<meta property="${tagName}" content="${tagValue}" />`
tagsStr += `<meta property="${tagName}" content="${escapeAttribute(tagValue)}" />`
})
// Standard
@ -101,7 +101,7 @@ export class TagsHtml {
const tagValue = standardMetaTags[tagName]
if (!tagValue) return
tagsStr += `<meta property="${tagName}" content="${tagValue}" />`
tagsStr += `<meta property="${tagName}" content="${escapeAttribute(tagValue)}" />`
})
// Twitter card
@ -109,12 +109,13 @@ export class TagsHtml {
const tagValue = twitterCardMetaTags[tagName]
if (!tagValue) return
tagsStr += `<meta property="${tagName}" content="${tagValue}" />`
tagsStr += `<meta property="${tagName}" content="${escapeAttribute(tagValue)}" />`
})
// OEmbed
for (const oembedLinkTag of oembedLinkTags) {
tagsStr += `<link rel="alternate" type="${oembedLinkTag.type}" href="${oembedLinkTag.href}" title="${oembedLinkTag.escapedTitle}" />`
// eslint-disable-next-line max-len
tagsStr += `<link rel="alternate" type="${oembedLinkTag.type}" href="${oembedLinkTag.href}" title="${escapeAttribute(oembedLinkTag.escapedTitle)}" />`
}
// Schema.org

View File

@ -16,7 +16,7 @@ function processFederateVideo (job: Job) {
const video = await VideoModel.loadFull(payload.videoUUID, t)
if (!video) return
return federateVideoIfNeeded(video, payload.isNewVideo, t)
return federateVideoIfNeeded(video, payload.isNewVideoForFederation, t)
})
})
}

View File

@ -60,6 +60,12 @@ async function processVideoLiveEnding (job: Job) {
return cleanupLiveAndFederate({ permanentLive, video, streamingPlaylistId: payload.streamingPlaylistId })
}
if (await hasReplayFiles(payload.replayDirectory) !== true) {
logger.info(`No replay files found for live ${video.uuid}, skipping video replay creation.`, { ...lTags(video.uuid) })
return cleanupLiveAndFederate({ permanentLive, video, streamingPlaylistId: payload.streamingPlaylistId })
}
if (permanentLive) {
await saveReplayToExternalVideo({
liveVideo: video,
@ -310,3 +316,7 @@ function createStoryboardJob (video: MVideo) {
}
})
}
async function hasReplayFiles (replayDirectory: string) {
return (await readdir(replayDirectory)).length !== 0
}

View File

@ -18,7 +18,7 @@ export async function generateWebVideoPresignedUrl (options: {
const command = new GetObjectCommand({
Bucket: CONFIG.OBJECT_STORAGE.WEB_VIDEOS.BUCKET_NAME,
Key: buildKey(key, CONFIG.OBJECT_STORAGE.WEB_VIDEOS),
ResponseContentDisposition: `attachment; filename=${downloadFilename}`
ResponseContentDisposition: `attachment; filename=${encodeURI(downloadFilename)}`
})
const url = await getSignedUrl(await getClient(), command, { expiresIn: 3600 * 24 })
@ -41,7 +41,7 @@ export async function generateHLSFilePresignedUrl (options: {
const command = new GetObjectCommand({
Bucket: CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS.BUCKET_NAME,
Key: buildKey(key, CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS),
ResponseContentDisposition: `attachment; filename=${downloadFilename}`
ResponseContentDisposition: `attachment; filename=${encodeURI(downloadFilename)}`
})
const url = await getSignedUrl(await getClient(), command, { expiresIn: 3600 * 24 })

View File

@ -48,7 +48,7 @@ export class UpdateVideosScheduler extends AbstractScheduler {
private async updateAVideo (schedule: MScheduleVideoUpdate) {
let oldPrivacy: VideoPrivacyType
let isNewVideo: boolean
let isNewVideoForFederation: boolean
let published = false
const video = await sequelizeTypescript.transaction(async t => {
@ -58,7 +58,7 @@ export class UpdateVideosScheduler extends AbstractScheduler {
logger.info('Executing scheduled video update on %s.', video.uuid)
if (schedule.privacy) {
isNewVideo = video.isNewVideo(schedule.privacy)
isNewVideoForFederation = video.isNewVideoForFederation(schedule.privacy)
oldPrivacy = video.privacy
setVideoPrivacy(video, schedule.privacy)
@ -78,7 +78,7 @@ export class UpdateVideosScheduler extends AbstractScheduler {
return { video, published: false }
}
await addVideoJobsAfterUpdate({ video, oldPrivacy, isNewVideo, nameChanged: false })
await addVideoJobsAfterUpdate({ video, oldPrivacy, isNewVideoForFederation, nameChanged: false })
return { video, published }
}

View File

@ -79,12 +79,18 @@ async function createChapters (options: {
}) {
const { chapters, transaction, videoId } = options
const existingTimecodes = new Set<number>()
for (const chapter of chapters) {
if (existingTimecodes.has(chapter.timecode)) continue
await VideoChapterModel.create({
title: chapter.title,
timecode: chapter.timecode,
videoId
}, { transaction })
existingTimecodes.add(chapter.timecode)
}
}

View File

@ -118,7 +118,7 @@ export async function onVideoStudioEnded (options: {
type: 'federate-video' as 'federate-video',
payload: {
videoUUID: video.uuid,
isNewVideo: false
isNewVideoForFederation: false
}
},

View File

@ -138,12 +138,12 @@ export const getCachedVideoDuration = memoizee(getVideoDuration, {
export async function addVideoJobsAfterUpdate (options: {
video: MVideoFullLight
isNewVideo: boolean
isNewVideoForFederation: boolean
nameChanged: boolean
oldPrivacy: VideoPrivacyType
}) {
const { video, nameChanged, oldPrivacy, isNewVideo } = options
const { video, nameChanged, oldPrivacy, isNewVideoForFederation } = options
const jobs: CreateJobArgument[] = []
const filePathChanged = await moveFilesIfPrivacyChanged(video, oldPrivacy)
@ -168,7 +168,7 @@ export async function addVideoJobsAfterUpdate (options: {
type: 'federate-video',
payload: {
videoUUID: video.uuid,
isNewVideo
isNewVideoForFederation
}
})

View File

@ -232,7 +232,7 @@ const videosUpdateValidator = getCommonVideoEditAttributes().concat([
if (!isValidPasswordProtectedPrivacy(req, res)) return cleanUpReqFiles(req)
const video = getVideoWithAttributes(res)
if (video.isLive && video.privacy !== req.body.privacy && video.state !== VideoState.WAITING_FOR_LIVE) {
if (exists(req.body.privacy) && video.isLive && video.privacy !== req.body.privacy && video.state !== VideoState.WAITING_FOR_LIVE) {
return res.fail({ message: 'Cannot update privacy of a live that has already started' })
}

View File

@ -2009,7 +2009,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
return isStateForFederation(this.state)
}
isNewVideo (newPrivacy: VideoPrivacyType) {
isNewVideoForFederation (newPrivacy: VideoPrivacyType) {
return this.hasPrivacyForFederation() === false && isPrivacyForFederation(newPrivacy) === true
}

View File

@ -50,8 +50,8 @@ export class LocalVideoViewerWatchSectionModel extends Model<Partial<AttributesO
for (const section of watchSections) {
const model = await this.create({
watchStart: section.start,
watchEnd: section.end,
watchStart: section.start || 0,
watchEnd: section.end || 0,
localVideoViewerId
}, { transaction })

View File

@ -7,7 +7,9 @@ import {
getLocalVideoActivityPubUrl,
getLocalVideoAnnounceActivityPubUrl,
getLocalVideoChannelActivityPubUrl,
getLocalVideoCommentActivityPubUrl
getLocalVideoCommentActivityPubUrl,
getLocalVideoPlaylistActivityPubUrl,
getLocalVideoPlaylistElementActivityPubUrl
} from '@server/lib/activitypub/url.js'
import { AccountModel } from '@server/models/account/account.js'
import { ActorFollowModel } from '@server/models/actor/actor-follow.js'
@ -17,6 +19,8 @@ import { VideoCommentModel } from '@server/models/video/video-comment.js'
import { VideoShareModel } from '@server/models/video/video-share.js'
import { VideoModel } from '@server/models/video/video.js'
import { MActorAccount } from '@server/types/models/index.js'
import { VideoPlaylistModel } from '@server/models/video/video-playlist.js'
import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element.js'
run()
.then(() => process.exit(0))
@ -122,6 +126,44 @@ async function run () {
await comment.save()
}
console.log('Updating video playlists.')
const videoPlaylists: VideoPlaylistModel[] = await VideoPlaylistModel.findAll({
include: [
{
model: AccountModel.unscoped(),
required: true,
include: [
{
model: ActorModel.unscoped(),
where: {
serverId: null
},
required: true
}
]
}
]
})
for (const playlist of videoPlaylists) {
console.log('Updating video playlist ' + playlist.url)
playlist.url = getLocalVideoPlaylistActivityPubUrl(playlist)
await playlist.save()
const elements: VideoPlaylistElementModel[] = await VideoPlaylistElementModel.findAll({
where: {
videoPlaylistId: playlist.id
}
})
for (const element of elements) {
console.log('Updating video playlist element ' + element.url)
element.url = getLocalVideoPlaylistElementActivityPubUrl(playlist, element)
await element.save()
}
}
console.log('Updating video and torrent files.')
const ids = await VideoModel.listLocalIds()

View File

@ -199,7 +199,7 @@ server {
alias /var/www/peertube/peertube-latest/client/dist/$1;
}
location ~ ^(/static/(webseed|web-videos|streaming-playlists)/private/)|^/download {
location ~ ^(/static/(webseed|web-videos|streaming-playlists/hls)/private/)|^/download {
# We can't rate limit a try_files directive, so we need to duplicate @api
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;