Compare commits

...

3 Commits

Author SHA1 Message Date
Chocobozzz
4fd4c91ec1 Add ability to cleanup views on local videos too 2025-10-10 11:52:33 +02:00
Chocobozzz
ed8279eae6 Copy codecs for HLS if possible 2025-10-10 09:05:57 +02:00
Chocobozzz
f04a11f74d Copy codecs for HLS transcoding
It was disabled because of Safari playback issues
(https://github.com/Chocobozzz/PeerTube/issues/3502), but I couldn't
reproduce so re-enable it

Allows to have a better audio/video quality (because we don't re-encode
these streams) and it saves CPU
2025-10-09 16:32:28 +02:00
23 changed files with 312 additions and 92 deletions

View File

@@ -18,6 +18,13 @@ import {
ProcessOptions,
scheduleTranscodingProgress
} from './common.js'
import {
canDoQuickAudioTranscode,
canDoQuickVideoTranscode,
ffprobePromise,
getVideoStreamDimensionsInfo,
getVideoStreamFPS
} from '@peertube/peertube-ffmpeg'
export async function processWebVideoTranscoding (options: ProcessOptions<RunnerJobVODWebVideoTranscodingPayload>) {
const { server, job, runnerToken } = options
@@ -46,7 +53,9 @@ export async function processWebVideoTranscoding (options: ProcessOptions<Runner
logger.info(`Downloaded input file ${payload.input.videoFileUrl} for job ${job.jobToken}. Running web video transcoding.`)
const ffmpegVod = buildFFmpegVOD({
onJobProgress: progress => { ffmpegProgress = progress }
onJobProgress: progress => {
ffmpegProgress = progress
}
})
await ffmpegVod.transcode({
@@ -108,15 +117,28 @@ export async function processHLSTranscoding (options: ProcessOptions<RunnerJobVO
videoInputPath = await downloadInputFile({ url: payload.input.videoFileUrl, runnerToken, job })
separatedAudioInputPath = await downloadSeparatedAudioFileIfNeeded({ urls: payload.input.separatedAudioFileUrl, runnerToken, job })
const inputProbe = await ffprobePromise(videoInputPath)
const { resolution } = await getVideoStreamDimensionsInfo(videoInputPath, inputProbe)
const fps = await getVideoStreamFPS(videoInputPath, inputProbe)
// Copy codecs if the input file can be quick transcoded (appropriate bitrate, codecs, etc.)
// And if the input resolution/fps are the same as the output resolution/fps
const copyCodecs = await canDoQuickAudioTranscode(videoInputPath, inputProbe) &&
await canDoQuickVideoTranscode(videoInputPath, fps) &&
resolution === payload.output.resolution &&
(!resolution || fps === payload.output.fps)
logger.info(`Downloaded input file ${payload.input.videoFileUrl} for job ${job.jobToken}. Running HLS transcoding.`)
const ffmpegVod = buildFFmpegVOD({
onJobProgress: progress => { ffmpegProgress = progress }
onJobProgress: progress => {
ffmpegProgress = progress
}
})
await ffmpegVod.transcode({
type: 'hls',
copyCodecs: false,
copyCodecs,
videoInputPath,
separatedAudioInputPath,
@@ -172,7 +194,7 @@ export async function processAudioMergeTranscoding (options: ProcessOptions<Runn
try {
logger.info(
`Downloading input files ${payload.input.audioFileUrl} and ${payload.input.previewFileUrl} ` +
`for audio merge transcoding job ${job.jobToken}`
`for audio merge transcoding job ${job.jobToken}`
)
audioPath = await downloadInputFile({ url: payload.input.audioFileUrl, runnerToken, job })
@@ -180,11 +202,13 @@ export async function processAudioMergeTranscoding (options: ProcessOptions<Runn
logger.info(
`Downloaded input files ${payload.input.audioFileUrl} and ${payload.input.previewFileUrl} ` +
`for job ${job.jobToken}. Running audio merge transcoding.`
`for job ${job.jobToken}. Running audio merge transcoding.`
)
const ffmpegVod = buildFFmpegVOD({
onJobProgress: progress => { ffmpegProgress = progress }
onJobProgress: progress => {
ffmpegProgress = progress
}
})
await ffmpegVod.transcode({

View File

@@ -25,7 +25,13 @@
<div class="stats-with-date">
<div class="overall-stats">
<h2>{{ getViewersStatsTitle() }}</h2>
<div class="title-container mb-4">
<h3 class="mb-0">{{ getViewersStatsTitle() }}</h3>
@if (hasMaxViewsAge()) {
<div class="muted-2 small" i18n>Views made before {{ getMaxViewsAgeDate() | date }} are not taken into account</div>
}
</div>
<div class="date-filter-wrapper">
<label class="visually-hidden" for="date-filter">Filter viewers stats by date</label>

View File

@@ -1,6 +1,6 @@
@use '_variables' as *;
@use '_mixins' as *;
@use 'nav' as *;
@use "_variables" as *;
@use "_mixins" as *;
@use "nav" as *;
.stats-embed {
display: flex;
@@ -12,8 +12,12 @@
display: flex;
flex-wrap: wrap;
h2 {
font-size: 16px;
.title-container {
h3 {
font-size: 16px;
font-weight: $font-bold;
}
width: 100%;
}
}

View File

@@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common'
import { Component, LOCALE_ID, OnInit, inject } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { ActivatedRoute } from '@angular/router'
import { Notifier, PeerTubeRouterService } from '@app/core'
import { Notifier, PeerTubeRouterService, ServerService } from '@app/core'
import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.component'
import { NumberFormatterPipe } from '@app/shared/shared-main/common/number-formatter.pipe'
import { LiveVideoService } from '@app/shared/shared-video-live/live-video.service'
@@ -83,6 +83,7 @@ export class VideoStatsComponent implements OnInit {
private numberFormatter = inject(NumberFormatterPipe)
private liveService = inject(LiveVideoService)
private manageController = inject(VideoManageController)
private serverService = inject(ServerService)
// Cannot handle date filters
globalStatsCards: Card[] = []
@@ -751,4 +752,18 @@ export class VideoStatsComponent implements OnInit {
second: 'numeric'
})
}
// ---------------------------------------------------------------------------
hasMaxViewsAge () {
return this.getMaxViewsAge() !== -1
}
getMaxViewsAgeDate () {
return new Date(Date.now() - this.getMaxViewsAge())
}
private getMaxViewsAge () {
return this.serverService.getHTMLConfig().views.videos.local.maxAge
}
}

View File

@@ -362,13 +362,21 @@ history:
views:
videos:
# PeerTube creates a database entry every hour for each video to track views over a period of time
# This is used in particular by the Trending page
# PeerTube could remove old remote video views if you want to reduce your database size (video view counter will not be altered)
# This is used in particular by the Trending/Hot algorithms
# PeerTube can remove views from remote videos if you want to reduce your database size (video view counter will not be altered)
# -1 means no cleanup
# Other values could be '6 months' or '30 days' etc (PeerTube will periodically delete old entries from database)
remote:
max_age: '30 days'
# PeerTube can also remove views informations from local videos
# Local views are used by the Trending/Hot algorithms, as remote views, but they are also used to display view stats to video makers
# Video view counter will not be altered, but video makers won't be able to see views stats of their videos before "max_age"
# -1 means no cleanup
# Other values could be '6 months' or '30 days' etc (PeerTube will periodically delete old entries from database)
local:
max_age: -1
# PeerTube buffers local video views before updating and federating the video
local_buffer_update_interval: '30 minutes'

View File

@@ -129,6 +129,9 @@ views:
remote:
max_age: -1
local:
max_age: '15 years'
watching_interval:
anonymous: '6 seconds'
users: '4 seconds'

View File

@@ -360,13 +360,21 @@ history:
views:
videos:
# PeerTube creates a database entry every hour for each video to track views over a period of time
# This is used in particular by the Trending page
# PeerTube could remove old remote video views if you want to reduce your database size (video view counter will not be altered)
# This is used in particular by the Trending/Hot algorithms
# PeerTube can remove views from remote videos if you want to reduce your database size (video view counter will not be altered)
# -1 means no cleanup
# Other values could be '6 months' or '30 days' etc (PeerTube will periodically delete old entries from database)
remote:
max_age: '30 days'
# PeerTube can also remove views informations from local videos
# Local views are used by the Trending/Hot algorithms, as remote views, but they are also used to display view stats to video makers
# Video view counter will not be altered, but video makers won't be able to see views stats of their videos before "max_age"
# -1 means no cleanup
# Other values could be '6 months' or '30 days' etc (PeerTube will periodically delete old entries from database)
local:
max_age: -1
# PeerTube buffers local video views before updating and federating the video
local_buffer_update_interval: '30 minutes'

View File

@@ -128,6 +128,8 @@ export async function canDoQuickAudioTranscode (path: string, probe?: FfprobeDat
export async function canDoQuickVideoTranscode (path: string, maxFPS: number, probe?: FfprobeData): Promise<boolean> {
const videoStream = await getVideoStream(path, probe)
if (!videoStream) return true
const fps = await getVideoStreamFPS(path, probe)
const bitRate = await getVideoStreamBitrate(path, probe)
const resolutionData = await getVideoStreamDimensionsInfo(path, probe)

View File

@@ -11,6 +11,7 @@ export type SendDebugCommand = {
| 'process-video-channel-sync-latest'
| 'process-update-videos-scheduler'
| 'remove-expired-user-exports'
| 'process-remove-old-views'
} | SendDebugTestEmails
export type SendDebugTestEmails = {

View File

@@ -408,6 +408,16 @@ export interface ServerConfig {
views: {
videos: {
remote: {
// milliseconds
maxAge: number
}
local: {
// milliseconds
maxAge: number
}
watchingInterval: {
// milliseconds
anonymous: number

View File

@@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { wait } from '@peertube/peertube-core-utils'
import { buildUUID } from '@peertube/peertube-node-utils'
import {
cleanupTests,
@@ -12,6 +11,7 @@ import {
waitJobs
} from '@peertube/peertube-server-commands'
import { SQLCommand } from '@tests/shared/sql-command.js'
import { processViewersStats } from '@tests/shared/views.js'
import { expect } from 'chai'
describe('Test video views cleaner', function () {
@@ -40,55 +40,118 @@ describe('Test video views cleaner', function () {
await servers[1].views.simulateView({ id: videoIdServer1, sessionId })
await servers[0].views.simulateView({ id: videoIdServer2, sessionId })
await servers[1].views.simulateView({ id: videoIdServer2, sessionId })
await processViewersStats(servers)
await waitJobs(servers)
sqlCommands = servers.map(s => new SQLCommand(s))
})
it('Should not clean old video views', async function () {
this.timeout(50000)
describe('Views on remote videos', function () {
it('Should not clean old video views', async function () {
this.timeout(50000)
await killallServers([ servers[0] ])
await killallServers([ servers[0] ])
await servers[0].run({ views: { videos: { remote: { max_age: '10 days' } } } })
await servers[0].debug.sendCommand({ body: { command: 'process-remove-old-views' } })
await servers[0].run({ views: { videos: { remote: { max_age: '10 days' } } } })
for (let i = 0; i < servers.length; i++) {
const total = await sqlCommands[i].countVideoViewsOf(videoIdServer1)
expect(total).to.equal(2, 'Server ' + servers[i].serverNumber + ' does not have the correct amount of views')
}
await wait(6000)
for (let i = 0; i < servers.length; i++) {
const total = await sqlCommands[i].countVideoViewsOf(videoIdServer2)
expect(total).to.equal(2, 'Server ' + servers[i].serverNumber + ' does not have the correct amount of views')
}
// Should still have views
const stats = await servers[0].videoStats.getOverallStats({ videoId: videoIdServer1 })
expect(stats.totalViewers).to.equal(2)
expect(stats.totalWatchTime).to.equal(10)
})
for (let i = 0; i < servers.length; i++) {
const total = await sqlCommands[i].countVideoViewsOf(videoIdServer1)
expect(total).to.equal(2, 'Server ' + servers[i].serverNumber + ' does not have the correct amount of views')
}
it('Should clean old video views', async function () {
this.timeout(50000)
for (let i = 0; i < servers.length; i++) {
const total = await sqlCommands[i].countVideoViewsOf(videoIdServer2)
expect(total).to.equal(2, 'Server ' + servers[i].serverNumber + ' does not have the correct amount of views')
}
await killallServers([ servers[0] ])
await servers[0].run({ views: { videos: { remote: { max_age: '5 seconds' } } } })
await servers[0].debug.sendCommand({ body: { command: 'process-remove-old-views' } })
for (let i = 0; i < servers.length; i++) {
const total = await sqlCommands[i].countVideoViewsOf(videoIdServer1)
expect(total).to.equal(2)
}
const totalServer1 = await sqlCommands[0].countVideoViewsOf(videoIdServer2)
expect(totalServer1).to.equal(0)
const totalServer2 = await sqlCommands[1].countVideoViewsOf(videoIdServer2)
expect(totalServer2).to.equal(2)
const stats = await servers[0].videoStats.getOverallStats({ videoId: videoIdServer1 })
expect(stats.totalViewers).to.equal(2)
expect(stats.totalWatchTime).to.equal(10)
})
})
it('Should clean old video views', async function () {
this.timeout(50000)
describe('Views on local videos', function () {
it('Should not clean old video views', async function () {
this.timeout(50000)
await killallServers([ servers[0] ])
await killallServers([ servers[0] ])
await servers[0].run({ views: { videos: { local: { max_age: '5 hours' } } } })
await servers[0].debug.sendCommand({ body: { command: 'process-remove-old-views' } })
await servers[0].run({ views: { videos: { remote: { max_age: '5 seconds' } } } })
for (let i = 0; i < servers.length; i++) {
const total = await sqlCommands[i].countVideoViewsOf(videoIdServer1)
expect(total).to.equal(2)
}
await wait(6000)
const totalServer1 = await sqlCommands[0].countVideoViewsOf(videoIdServer2)
expect(totalServer1).to.equal(0)
// Should still have views
const totalServer2 = await sqlCommands[1].countVideoViewsOf(videoIdServer2)
expect(totalServer2).to.equal(2)
for (let i = 0; i < servers.length; i++) {
const total = await sqlCommands[i].countVideoViewsOf(videoIdServer1)
expect(total).to.equal(2)
}
const stats = await servers[0].videoStats.getOverallStats({ videoId: videoIdServer1 })
expect(stats.totalViewers).to.equal(2)
expect(stats.totalWatchTime).to.equal(10)
})
const totalServer1 = await sqlCommands[0].countVideoViewsOf(videoIdServer2)
expect(totalServer1).to.equal(0)
it('Should clean old video views', async function () {
this.timeout(50000)
const totalServer2 = await sqlCommands[1].countVideoViewsOf(videoIdServer2)
expect(totalServer2).to.equal(2)
await killallServers([ servers[0] ])
await servers[0].run({ views: { videos: { local: { max_age: '5 seconds' } } } })
await servers[0].debug.sendCommand({ body: { command: 'process-remove-old-views' } })
{
const totalServer1 = await sqlCommands[0].countVideoViewsOf(videoIdServer1)
expect(totalServer1).to.equal(0)
const totalServer2 = await sqlCommands[1].countVideoViewsOf(videoIdServer1)
expect(totalServer2).to.equal(2)
}
{
const totalServer1 = await sqlCommands[0].countVideoViewsOf(videoIdServer2)
expect(totalServer1).to.equal(0)
const totalServer2 = await sqlCommands[1].countVideoViewsOf(videoIdServer2)
expect(totalServer2).to.equal(2)
}
const stats = await servers[0].videoStats.getOverallStats({ videoId: videoIdServer1 })
expect(stats.totalViewers).to.equal(0)
expect(stats.totalWatchTime).to.equal(0)
})
})
describe('Views counter', function () {
it('Should still have the appropriate views counter', async function () {
const { views } = await servers[0].videos.get({ id: videoIdServer1 })
expect(views).to.equal(2)
})
})
after(async function () {

View File

@@ -61,6 +61,7 @@ describe('Parse duration', function () {
expect(parseDurationToMs('1 minute')).to.equal(60 * 1000)
expect(parseDurationToMs('1 hour')).to.equal(3600 * 1000)
expect(parseDurationToMs('35 hours')).to.equal(3600 * 35 * 1000)
expect(parseDurationToMs('15 years')).to.equal(15 * 3600 * 24 * 365 * 1000)
})
it('Should be invalid when given invalid value', function () {

View File

@@ -21,6 +21,7 @@ import { VideoViewsBufferScheduler } from '@server/lib/schedulers/video-views-bu
import { VideoViewsManager } from '@server/lib/views/video-views-manager.js'
import express from 'express'
import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../../middlewares/index.js'
import { RemoveOldViewsScheduler } from '@server/lib/schedulers/remove-old-views-scheduler.js'
const debugRouter = express.Router()
@@ -63,6 +64,7 @@ async function runCommand (req: express.Request, res: express.Response) {
'process-video-viewers': () => VideoViewsManager.Instance.processViewerStats(),
'process-update-videos-scheduler': () => UpdateVideosScheduler.Instance.execute(),
'process-video-channel-sync-latest': () => VideoChannelSyncLatestScheduler.Instance.execute(),
'process-remove-old-views': () => RemoveOldViewsScheduler.Instance.execute(),
'test-emails': () => testEmails(req, res)
}

View File

@@ -50,7 +50,8 @@ const timeTable = {
hour: 3600000,
day: 3600000 * 24,
week: 3600000 * 24 * 7,
month: 3600000 * 24 * 30
month: 3600000 * 24 * 30,
year: 3600000 * 24 * 365
}
export function parseDurationToMs (duration: number | string): number {
@@ -68,7 +69,10 @@ export function parseDurationToMs (duration: number | string): number {
unit = 'ms'
}
return (len || 1) * (timeTable[unit] || 0)
const multiplier = timeTable[unit]
if (!multiplier) throw new Error('Cannot parse datetime unit ' + unit)
return (len || 1) * multiplier
}
}
@@ -271,19 +275,19 @@ const pipelinePromise = promisify(pipeline)
// ---------------------------------------------------------------------------
export {
objectConverter,
mapToJSON,
sanitizeUrl,
sanitizeHost,
execShell,
pageToStartAndCount,
peertubeTruncate,
scryptPromise,
randomBytesPromise,
generateRSAKeyPairPromise,
generateED25519KeyPairPromise,
execPromise2,
execPromise,
execPromise2,
execShell,
generateED25519KeyPairPromise,
generateRSAKeyPairPromise,
mapToJSON,
objectConverter,
pageToStartAndCount,
parseSemVersion,
peertubeTruncate,
pipelinePromise,
parseSemVersion
randomBytesPromise,
sanitizeHost,
sanitizeUrl,
scryptPromise
}

View File

@@ -398,6 +398,9 @@ const CONFIG = {
REMOTE: {
MAX_AGE: parseDurationToMs(config.get('views.videos.remote.max_age'))
},
LOCAL: {
MAX_AGE: parseDurationToMs(config.get('views.videos.local.max_age'))
},
LOCAL_BUFFER_UPDATE_INTERVAL: parseDurationToMs(config.get('views.videos.local_buffer_update_interval')),
VIEW_EXPIRATION: parseDurationToMs(config.get('views.videos.view_expiration')),
COUNT_VIEW_AFTER: parseDurationToMs(config.get<number>('views.videos.count_view_after')),

View File

@@ -1200,6 +1200,10 @@ export const STATS_TIMESERIE = {
// ---------------------------------------------------------------------------
export const MAX_SQL_DELETE_ITEMS = 10000
// ---------------------------------------------------------------------------
// Special constants for a test instance
if (process.env.PRODUCTION_CONSTANTS !== 'true') {
if (isTestOrDevInstance()) {
@@ -1215,7 +1219,6 @@ if (process.env.PRODUCTION_CONSTANTS !== 'true') {
SCHEDULER_INTERVALS_MS.ACTOR_FOLLOW_SCORES = 1000
SCHEDULER_INTERVALS_MS.REMOVE_OLD_JOBS = 10000
SCHEDULER_INTERVALS_MS.REMOVE_OLD_HISTORY = 5000
SCHEDULER_INTERVALS_MS.REMOVE_OLD_VIEWS = 5000
SCHEDULER_INTERVALS_MS.UPDATE_VIDEOS = 5000
SCHEDULER_INTERVALS_MS.AUTO_FOLLOW_INDEX_INSTANCES = 5000
SCHEDULER_INTERVALS_MS.UPDATE_INBOX_STATS = 5000

View File

@@ -1,3 +1,4 @@
import { LocalVideoViewerModel } from '@server/models/view/local-video-viewer.js'
import { VideoViewModel } from '@server/models/view/video-view.js'
import { logger } from '../../helpers/logger.js'
import { CONFIG } from '../../initializers/config.js'
@@ -5,7 +6,6 @@ import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants.js'
import { AbstractScheduler } from './abstract-scheduler.js'
export class RemoveOldViewsScheduler extends AbstractScheduler {
private static instance: AbstractScheduler
protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.REMOVE_OLD_VIEWS
@@ -14,15 +14,32 @@ export class RemoveOldViewsScheduler extends AbstractScheduler {
super()
}
protected internalExecute () {
if (CONFIG.VIEWS.VIDEOS.REMOTE.MAX_AGE === -1) return
protected async internalExecute () {
await this.removeRemoteViews()
await this.removeLocalViews()
}
logger.info('Removing old videos views.')
private removeRemoteViews () {
if (CONFIG.VIEWS.VIDEOS.REMOTE.MAX_AGE <= 0) return
logger.info('Removing old views from remote videos.')
const now = new Date()
const beforeDate = new Date(now.getTime() - CONFIG.VIEWS.VIDEOS.REMOTE.MAX_AGE).toISOString()
return VideoViewModel.removeOldRemoteViewsHistory(beforeDate)
return VideoViewModel.removeOldRemoteViews(beforeDate)
}
private async removeLocalViews () {
if (CONFIG.VIEWS.VIDEOS.LOCAL.MAX_AGE <= 0) return
logger.info('Removing old views from local videos.')
const now = new Date()
const beforeDate = new Date(now.getTime() - CONFIG.VIEWS.VIDEOS.LOCAL.MAX_AGE).toISOString()
await VideoViewModel.removeOldLocalViews(beforeDate)
await LocalVideoViewerModel.removeOldViews(beforeDate)
}
static get Instance () {

View File

@@ -382,6 +382,14 @@ class ServerConfigManager {
views: {
videos: {
remote: {
maxAge: CONFIG.VIEWS.VIDEOS.REMOTE.MAX_AGE
},
local: {
maxAge: CONFIG.VIEWS.VIDEOS.LOCAL.MAX_AGE
},
watchingInterval: {
anonymous: CONFIG.VIEWS.VIDEOS.WATCHING_INTERVAL.ANONYMOUS,
users: CONFIG.VIEWS.VIDEOS.WATCHING_INTERVAL.USERS

View File

@@ -12,8 +12,7 @@ import { buildOriginalFileResolution, computeResolutionsToTranscode } from '../.
const lTags = loggerTagsFactory('transcoding')
export abstract class AbstractJobBuilder <P> {
export abstract class AbstractJobBuilder<P> {
async createOptimizeOrMergeAudioJobs (options: {
video: MVideoFullLight
videoFile: MVideoFile
@@ -74,9 +73,6 @@ export abstract class AbstractJobBuilder <P> {
if (CONFIG.TRANSCODING.HLS.ENABLED === true) {
const hasSplitAudioTranscoding = CONFIG.TRANSCODING.HLS.SPLIT_AUDIO_AND_VIDEO && videoFile.hasAudio()
// We had some issues with a web video quick transcoded while producing a HLS version of it
const copyCodecs = !quickTranscode
const hlsPayloads: (P & { higherPriority?: boolean })[] = []
hlsPayloads.push(
@@ -85,7 +81,7 @@ export abstract class AbstractJobBuilder <P> {
separatedAudio: hasSplitAudioTranscoding,
copyCodecs,
copyCodecs: true,
resolution: maxResolution,
fps: maxFPS,
@@ -105,7 +101,7 @@ export abstract class AbstractJobBuilder <P> {
deleteWebVideoFiles: !CONFIG.TRANSCODING.WEB_VIDEOS.ENABLED,
separatedAudio: hasSplitAudioTranscoding,
copyCodecs,
copyCodecs: true,
resolution: 0,
fps: 0,
video,
@@ -183,7 +179,7 @@ export abstract class AbstractJobBuilder <P> {
: this.buildWebVideoJobPayload({ video, resolution: maxResolution, fps, isNewVideo })
// Low resolutions use the biggest one as ffmpeg input so we need to process max resolution (with audio) independently
const payloads: [ [ P ], ...(P[][]) ] = [ [ parent ] ]
const payloads: [[P], ...(P[][])] = [ [ parent ] ]
// Process audio first to not override the max resolution where the audio stream will be removed
if (transcodingType === 'hls' && separatedAudio) {
@@ -267,7 +263,7 @@ export abstract class AbstractJobBuilder <P> {
protected abstract createJobs (options: {
video: MVideoFullLight
// Array of sequential jobs to create that depend on parent job
payloads: [ [ (P & { higherPriority?: boolean }) ], ...((P & { higherPriority?: boolean })[][]) ]
payloads: [[(P & { higherPriority?: boolean })], ...((P & { higherPriority?: boolean })[][])]
user: MUserId | null
}): Promise<void>
@@ -304,5 +300,4 @@ export abstract class AbstractJobBuilder <P> {
fps: number
isNewVideo: boolean
}): P
}

View File

@@ -75,7 +75,7 @@ export const videoTimeseriesStatsValidator = [
if (query.startDate && getIntervalByDays(query.startDate, query.endDate) > STATS_TIMESERIE.MAX_DAYS) {
return res.fail({
status: HttpStatusCode.BAD_REQUEST_400,
message: 'Star date and end date interval is too big'
message: 'Start date and end date interval is too big'
})
}

View File

@@ -1,6 +1,7 @@
import { forceNumber } from '@peertube/peertube-core-utils'
import { FollowState } from '@peertube/peertube-models'
import { AttributesOnly } from '@peertube/peertube-typescript-utils'
import { MAX_SQL_DELETE_ITEMS } from '@server/initializers/constants.js'
import { literal, Model, ModelStatic } from 'sequelize'
import { Literal } from 'sequelize/types/utils'
@@ -72,3 +73,11 @@ export function buildSQLAttributes<M extends Model> (options: {
return builtAttributes
}
export async function safeBulkDestroy (destroyFn: () => Promise<number>) {
const destroyedRows = await destroyFn()
if (destroyedRows === MAX_SQL_DELETE_ITEMS) {
return safeBulkDestroy(destroyFn)
}
}

View File

@@ -6,12 +6,13 @@ import {
VideoStatsUserAgent,
WatchActionObject
} from '@peertube/peertube-models'
import { MAX_SQL_DELETE_ITEMS } from '@server/initializers/constants.js'
import { getActivityStreamDuration } from '@server/lib/activitypub/activity.js'
import { buildGroupByAndBoundaries } from '@server/lib/timeserie.js'
import { MLocalVideoViewer, MLocalVideoViewerWithWatchSections, MVideo } from '@server/types/models/index.js'
import { QueryOptionsWithType, QueryTypes } from 'sequelize'
import { Op, QueryOptionsWithType, QueryTypes } from 'sequelize'
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, HasMany, IsUUID, Table } from 'sequelize-typescript'
import { SequelizeModel } from '../shared/index.js'
import { safeBulkDestroy, SequelizeModel } from '../shared/index.js'
import { VideoModel } from '../video/video.js'
import { LocalVideoViewerWatchSectionModel } from './local-video-viewer-watch-section.js'
@@ -434,4 +435,19 @@ export class LocalVideoViewerModel extends SequelizeModel<LocalVideoViewerModel>
...location
}
}
// ---------------------------------------------------------------------------
static removeOldViews (beforeDate: string) {
return safeBulkDestroy(() => {
return LocalVideoViewerModel.destroy({
where: {
startDate: {
[Op.lt]: beforeDate
}
},
limit: MAX_SQL_DELETE_ITEMS
})
})
}
}

View File

@@ -1,7 +1,8 @@
import { MAX_SQL_DELETE_ITEMS } from '@server/initializers/constants.js'
import { literal, Op } from 'sequelize'
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Table } from 'sequelize-typescript'
import { safeBulkDestroy, SequelizeModel } from '../shared/index.js'
import { VideoModel } from '../video/video.js'
import { SequelizeModel } from '../shared/index.js'
/**
* Aggregate views of all videos federated with our instance
@@ -48,18 +49,35 @@ export class VideoViewModel extends SequelizeModel<VideoViewModel> {
})
declare Video: Awaited<VideoModel>
static removeOldRemoteViewsHistory (beforeDate: string) {
const query = {
where: {
startDate: {
[Op.lt]: beforeDate
static removeOldRemoteViews (beforeDate: string) {
return safeBulkDestroy(() => {
return VideoViewModel.destroy({
where: {
startDate: {
[Op.lt]: beforeDate
},
videoId: {
[Op.in]: literal('(SELECT "id" FROM "video" WHERE "remote" IS TRUE)')
}
},
videoId: {
[Op.in]: literal('(SELECT "id" FROM "video" WHERE "remote" IS TRUE)')
}
}
}
limit: MAX_SQL_DELETE_ITEMS
})
})
}
return VideoViewModel.destroy(query)
static removeOldLocalViews (beforeDate: string) {
return safeBulkDestroy(() => {
return VideoViewModel.destroy({
where: {
startDate: {
[Op.lt]: beforeDate
},
videoId: {
[Op.in]: literal('(SELECT "id" FROM "video" WHERE "remote" IS FALSE)')
}
},
limit: MAX_SQL_DELETE_ITEMS
})
})
}
}