Add ability to delete old remote views
This commit is contained in:
parent
7b293f2868
commit
cda03765fe
|
@ -118,6 +118,16 @@ history:
|
||||||
# Other values could be '6 months' or '30 days' etc (PeerTube will periodically delete old entries from database)
|
# Other values could be '6 months' or '30 days' etc (PeerTube will periodically delete old entries from database)
|
||||||
max_age: -1
|
max_age: -1
|
||||||
|
|
||||||
|
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)
|
||||||
|
# -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: -1
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
previews:
|
previews:
|
||||||
size: 500 # Max number of previews you want to cache
|
size: 500 # Max number of previews you want to cache
|
||||||
|
|
|
@ -119,6 +119,17 @@ history:
|
||||||
# Other values could be '6 months' or '30 days' etc (PeerTube will periodically delete old entries from database)
|
# Other values could be '6 months' or '30 days' etc (PeerTube will periodically delete old entries from database)
|
||||||
max_age: -1
|
max_age: -1
|
||||||
|
|
||||||
|
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)
|
||||||
|
# -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: -1
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
#
|
#
|
||||||
# From this point, all the following keys can be overridden by the web interface
|
# From this point, all the following keys can be overridden by the web interface
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { createInterface } from 'readline'
|
||||||
import * as winston from 'winston'
|
import * as winston from 'winston'
|
||||||
import { labelFormatter } from '../server/helpers/logger'
|
import { labelFormatter } from '../server/helpers/logger'
|
||||||
import { CONFIG } from '../server/initializers/config'
|
import { CONFIG } from '../server/initializers/config'
|
||||||
import { mtimeSortFilesDesc } from '../shared/utils/logs/logs'
|
import { mtimeSortFilesDesc } from '../shared/core-utils/logs/logs'
|
||||||
|
|
||||||
program
|
program
|
||||||
.option('-l, --level [level]', 'Level log (debug/info/warn/error)')
|
.option('-l, --level [level]', 'Level log (debug/info/warn/error)')
|
||||||
|
|
|
@ -101,6 +101,7 @@ import {
|
||||||
import { advertiseDoNotTrack } from './server/middlewares/dnt'
|
import { advertiseDoNotTrack } from './server/middlewares/dnt'
|
||||||
import { Redis } from './server/lib/redis'
|
import { Redis } from './server/lib/redis'
|
||||||
import { ActorFollowScheduler } from './server/lib/schedulers/actor-follow-scheduler'
|
import { ActorFollowScheduler } from './server/lib/schedulers/actor-follow-scheduler'
|
||||||
|
import { RemoveOldViewsScheduler } from './server/lib/schedulers/remove-old-views-scheduler'
|
||||||
import { RemoveOldJobsScheduler } from './server/lib/schedulers/remove-old-jobs-scheduler'
|
import { RemoveOldJobsScheduler } from './server/lib/schedulers/remove-old-jobs-scheduler'
|
||||||
import { UpdateVideosScheduler } from './server/lib/schedulers/update-videos-scheduler'
|
import { UpdateVideosScheduler } from './server/lib/schedulers/update-videos-scheduler'
|
||||||
import { YoutubeDlUpdateScheduler } from './server/lib/schedulers/youtube-dl-update-scheduler'
|
import { YoutubeDlUpdateScheduler } from './server/lib/schedulers/youtube-dl-update-scheduler'
|
||||||
|
@ -242,6 +243,7 @@ async function startApplication () {
|
||||||
YoutubeDlUpdateScheduler.Instance.enable()
|
YoutubeDlUpdateScheduler.Instance.enable()
|
||||||
VideosRedundancyScheduler.Instance.enable()
|
VideosRedundancyScheduler.Instance.enable()
|
||||||
RemoveOldHistoryScheduler.Instance.enable()
|
RemoveOldHistoryScheduler.Instance.enable()
|
||||||
|
RemoveOldViewsScheduler.Instance.enable()
|
||||||
|
|
||||||
// Redis initialization
|
// Redis initialization
|
||||||
Redis.Instance.init()
|
Redis.Instance.init()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
import { UserRight } from '../../../../shared/models/users'
|
import { UserRight } from '../../../../shared/models/users'
|
||||||
import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../../middlewares'
|
import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../../middlewares'
|
||||||
import { mtimeSortFilesDesc } from '../../../../shared/utils/logs/logs'
|
import { mtimeSortFilesDesc } from '../../../../shared/core-utils/logs/logs'
|
||||||
import { readdir, readFile } from 'fs-extra'
|
import { readdir, readFile } from 'fs-extra'
|
||||||
import { MAX_LOGS_OUTPUT_CHARACTERS } from '../../../initializers/constants'
|
import { MAX_LOGS_OUTPUT_CHARACTERS } from '../../../initializers/constants'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
|
|
@ -26,7 +26,8 @@ function checkMissedConfig () {
|
||||||
'instance.is_nsfw', 'instance.default_nsfw_policy', 'instance.robots', 'instance.securitytxt',
|
'instance.is_nsfw', 'instance.default_nsfw_policy', 'instance.robots', 'instance.securitytxt',
|
||||||
'services.twitter.username', 'services.twitter.whitelisted',
|
'services.twitter.username', 'services.twitter.whitelisted',
|
||||||
'followers.instance.enabled', 'followers.instance.manual_approval',
|
'followers.instance.enabled', 'followers.instance.manual_approval',
|
||||||
'tracker.enabled', 'tracker.private', 'tracker.reject_too_many_announces'
|
'tracker.enabled', 'tracker.private', 'tracker.reject_too_many_announces',
|
||||||
|
'history.videos.max_age', 'views.videos.remote.max_age'
|
||||||
]
|
]
|
||||||
const requiredAlternatives = [
|
const requiredAlternatives = [
|
||||||
[ // set
|
[ // set
|
||||||
|
|
|
@ -99,6 +99,13 @@ const CONFIG = {
|
||||||
MAX_AGE: parseDurationToMs(config.get('history.videos.max_age'))
|
MAX_AGE: parseDurationToMs(config.get('history.videos.max_age'))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
VIEWS: {
|
||||||
|
VIDEOS: {
|
||||||
|
REMOTE: {
|
||||||
|
MAX_AGE: parseDurationToMs(config.get('views.videos.remote.max_age'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
ADMIN: {
|
ADMIN: {
|
||||||
get EMAIL () { return config.get<string>('admin.email') }
|
get EMAIL () { return config.get<string>('admin.email') }
|
||||||
},
|
},
|
||||||
|
|
|
@ -163,6 +163,7 @@ const SCHEDULER_INTERVALS_MS = {
|
||||||
removeOldJobs: 60000 * 60, // 1 hour
|
removeOldJobs: 60000 * 60, // 1 hour
|
||||||
updateVideos: 60000, // 1 minute
|
updateVideos: 60000, // 1 minute
|
||||||
youtubeDLUpdate: 60000 * 60 * 24, // 1 day
|
youtubeDLUpdate: 60000 * 60 * 24, // 1 day
|
||||||
|
removeOldViews: 60000 * 60 * 24, // 1 day
|
||||||
removeOldHistory: 60000 * 60 * 24 // 1 day
|
removeOldHistory: 60000 * 60 * 24 // 1 day
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -592,6 +593,7 @@ if (isTestInstance() === true) {
|
||||||
SCHEDULER_INTERVALS_MS.actorFollowScores = 1000
|
SCHEDULER_INTERVALS_MS.actorFollowScores = 1000
|
||||||
SCHEDULER_INTERVALS_MS.removeOldJobs = 10000
|
SCHEDULER_INTERVALS_MS.removeOldJobs = 10000
|
||||||
SCHEDULER_INTERVALS_MS.removeOldHistory = 5000
|
SCHEDULER_INTERVALS_MS.removeOldHistory = 5000
|
||||||
|
SCHEDULER_INTERVALS_MS.removeOldViews = 5000
|
||||||
SCHEDULER_INTERVALS_MS.updateVideos = 5000
|
SCHEDULER_INTERVALS_MS.updateVideos = 5000
|
||||||
REPEAT_JOBS[ 'videos-views' ] = { every: 5000 }
|
REPEAT_JOBS[ 'videos-views' ] = { every: 5000 }
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { logger } from '../../helpers/logger'
|
||||||
|
import { AbstractScheduler } from './abstract-scheduler'
|
||||||
|
import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
|
||||||
|
import { UserVideoHistoryModel } from '../../models/account/user-video-history'
|
||||||
|
import { CONFIG } from '../../initializers/config'
|
||||||
|
import { isTestInstance } from '../../helpers/core-utils'
|
||||||
|
import { VideoViewModel } from '../../models/video/video-views'
|
||||||
|
|
||||||
|
export class RemoveOldViewsScheduler extends AbstractScheduler {
|
||||||
|
|
||||||
|
private static instance: AbstractScheduler
|
||||||
|
|
||||||
|
protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.removeOldViews
|
||||||
|
|
||||||
|
private constructor () {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected internalExecute () {
|
||||||
|
if (CONFIG.VIEWS.VIDEOS.REMOTE.MAX_AGE === -1) return
|
||||||
|
|
||||||
|
logger.info('Removing old videos views.')
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
const beforeDate = new Date(now.getTime() - CONFIG.VIEWS.VIDEOS.REMOTE.MAX_AGE).toISOString()
|
||||||
|
|
||||||
|
return VideoViewModel.removeOldRemoteViewsHistory(beforeDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
static get Instance () {
|
||||||
|
return this.instance || (this.instance = new this())
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,4 +41,18 @@ export class VideoViewModel extends Model<VideoViewModel> {
|
||||||
})
|
})
|
||||||
Video: VideoModel
|
Video: VideoModel
|
||||||
|
|
||||||
|
static removeOldRemoteViewsHistory (beforeDate: string) {
|
||||||
|
const query = {
|
||||||
|
where: {
|
||||||
|
startDate: {
|
||||||
|
[Sequelize.Op.lt]: beforeDate
|
||||||
|
},
|
||||||
|
videoId: {
|
||||||
|
[Sequelize.Op.in]: Sequelize.literal('(SELECT "id" FROM "video" WHERE "remote" IS TRUE)')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return VideoViewModel.destroy(query)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,3 +18,4 @@ import './video-transcoder'
|
||||||
import './videos-filter'
|
import './videos-filter'
|
||||||
import './videos-history'
|
import './videos-history'
|
||||||
import './videos-overview'
|
import './videos-overview'
|
||||||
|
import './videos-views-cleaner'
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
/* tslint:disable:no-unused-expression */
|
||||||
|
|
||||||
|
import * as chai from 'chai'
|
||||||
|
import 'mocha'
|
||||||
|
import {
|
||||||
|
flushAndRunMultipleServers,
|
||||||
|
flushTests,
|
||||||
|
killallServers,
|
||||||
|
reRunServer,
|
||||||
|
runServer,
|
||||||
|
ServerInfo,
|
||||||
|
setAccessTokensToServers,
|
||||||
|
uploadVideo, uploadVideoAndGetId, viewVideo, wait, countVideoViewsOf, doubleFollow, waitJobs
|
||||||
|
} from '../../../../shared/utils'
|
||||||
|
import { getVideosOverview } from '../../../../shared/utils/overviews/overviews'
|
||||||
|
import { VideosOverview } from '../../../../shared/models/overviews'
|
||||||
|
import { listMyVideosHistory } from '../../../../shared/utils/videos/video-history'
|
||||||
|
|
||||||
|
const expect = chai.expect
|
||||||
|
|
||||||
|
describe('Test video views cleaner', function () {
|
||||||
|
let servers: ServerInfo[]
|
||||||
|
|
||||||
|
let videoIdServer1: string
|
||||||
|
let videoIdServer2: string
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(50000)
|
||||||
|
|
||||||
|
await flushTests()
|
||||||
|
|
||||||
|
servers = await flushAndRunMultipleServers(2)
|
||||||
|
await setAccessTokensToServers(servers)
|
||||||
|
|
||||||
|
await doubleFollow(servers[0], servers[1])
|
||||||
|
|
||||||
|
videoIdServer1 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video server 1' })).uuid
|
||||||
|
videoIdServer2 = (await uploadVideoAndGetId({ server: servers[1], videoName: 'video server 2' })).uuid
|
||||||
|
|
||||||
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
await viewVideo(servers[0].url, videoIdServer1)
|
||||||
|
await viewVideo(servers[1].url, videoIdServer1)
|
||||||
|
await viewVideo(servers[0].url, videoIdServer2)
|
||||||
|
await viewVideo(servers[1].url, videoIdServer2)
|
||||||
|
|
||||||
|
await waitJobs(servers)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should not clean old video views', async function () {
|
||||||
|
this.timeout(50000)
|
||||||
|
|
||||||
|
killallServers([ servers[0] ])
|
||||||
|
|
||||||
|
await reRunServer(servers[0], { views: { videos: { remote: { max_age: '10 days' } } } })
|
||||||
|
|
||||||
|
await wait(6000)
|
||||||
|
|
||||||
|
// Should still have views
|
||||||
|
|
||||||
|
{
|
||||||
|
for (const server of servers) {
|
||||||
|
const total = await countVideoViewsOf(server.serverNumber, videoIdServer1)
|
||||||
|
expect(total).to.equal(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
for (const server of servers) {
|
||||||
|
const total = await countVideoViewsOf(server.serverNumber, videoIdServer2)
|
||||||
|
expect(total).to.equal(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should clean old video views', async function () {
|
||||||
|
this.timeout(50000)
|
||||||
|
|
||||||
|
this.timeout(50000)
|
||||||
|
|
||||||
|
killallServers([ servers[0] ])
|
||||||
|
|
||||||
|
await reRunServer(servers[0], { views: { videos: { remote: { max_age: '5 seconds' } } } })
|
||||||
|
|
||||||
|
await wait(6000)
|
||||||
|
|
||||||
|
// Should still have views
|
||||||
|
|
||||||
|
{
|
||||||
|
for (const server of servers) {
|
||||||
|
const total = await countVideoViewsOf(server.serverNumber, videoIdServer1)
|
||||||
|
expect(total).to.equal(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const totalServer1 = await countVideoViewsOf(servers[0].serverNumber, videoIdServer2)
|
||||||
|
expect(totalServer1).to.equal(0)
|
||||||
|
|
||||||
|
const totalServer2 = await countVideoViewsOf(servers[1].serverNumber, videoIdServer2)
|
||||||
|
expect(totalServer2).to.equal(2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async function () {
|
||||||
|
killallServers(servers)
|
||||||
|
|
||||||
|
// Keep the logs if the test failed
|
||||||
|
if (this['ok']) {
|
||||||
|
await flushTests()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { stat } from 'fs-extra'
|
||||||
|
|
||||||
|
async function mtimeSortFilesDesc (files: string[], basePath: string) {
|
||||||
|
const promises = []
|
||||||
|
const out: { file: string, mtime: number }[] = []
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const p = stat(basePath + '/' + file)
|
||||||
|
.then(stats => {
|
||||||
|
if (stats.isFile()) out.push({ file, mtime: stats.mtime.getTime() })
|
||||||
|
})
|
||||||
|
|
||||||
|
promises.push(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises)
|
||||||
|
|
||||||
|
out.sort((a, b) => b.mtime - a.mtime)
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
mtimeSortFilesDesc
|
||||||
|
}
|
|
@ -1,28 +1,6 @@
|
||||||
// Thanks: https://stackoverflow.com/a/37014317
|
|
||||||
import { stat } from 'fs-extra'
|
|
||||||
import { makeGetRequest } from '../requests/requests'
|
import { makeGetRequest } from '../requests/requests'
|
||||||
import { LogLevel } from '../../models/server/log-level.type'
|
import { LogLevel } from '../../models/server/log-level.type'
|
||||||
|
|
||||||
async function mtimeSortFilesDesc (files: string[], basePath: string) {
|
|
||||||
const promises = []
|
|
||||||
const out: { file: string, mtime: number }[] = []
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const p = stat(basePath + '/' + file)
|
|
||||||
.then(stats => {
|
|
||||||
if (stats.isFile()) out.push({ file, mtime: stats.mtime.getTime() })
|
|
||||||
})
|
|
||||||
|
|
||||||
promises.push(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(promises)
|
|
||||||
|
|
||||||
out.sort((a, b) => b.mtime - a.mtime)
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLogs (url: string, accessToken: string, startDate: Date, endDate?: Date, level?: LogLevel) {
|
function getLogs (url: string, accessToken: string, startDate: Date, endDate?: Date, level?: LogLevel) {
|
||||||
const path = '/api/v1/server/logs'
|
const path = '/api/v1/server/logs'
|
||||||
|
|
||||||
|
@ -36,6 +14,5 @@ function getLogs (url: string, accessToken: string, startDate: Date, endDate?: D
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
mtimeSortFilesDesc,
|
|
||||||
getLogs
|
getLogs
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,20 @@ function setPlaylistField (serverNumber: number, uuid: string, field: string, va
|
||||||
return seq.query(`UPDATE "videoPlaylist" SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options)
|
return seq.query(`UPDATE "videoPlaylist" SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function countVideoViewsOf (serverNumber: number, uuid: string) {
|
||||||
|
const seq = getSequelize(serverNumber)
|
||||||
|
|
||||||
|
// tslint:disable
|
||||||
|
const query = `SELECT SUM("videoView"."views") AS "total" FROM "videoView" INNER JOIN "video" ON "video"."id" = "videoView"."videoId" WHERE "video"."uuid" = '${uuid}'`
|
||||||
|
|
||||||
|
const options = { type: Sequelize.QueryTypes.SELECT }
|
||||||
|
const [ { total } ] = await seq.query(query, options)
|
||||||
|
|
||||||
|
if (!total) return 0
|
||||||
|
|
||||||
|
return parseInt(total, 10)
|
||||||
|
}
|
||||||
|
|
||||||
async function closeAllSequelize (servers: any[]) {
|
async function closeAllSequelize (servers: any[]) {
|
||||||
for (let i = 1; i <= servers.length; i++) {
|
for (let i = 1; i <= servers.length; i++) {
|
||||||
if (sequelizes[ i ]) {
|
if (sequelizes[ i ]) {
|
||||||
|
@ -61,5 +75,6 @@ export {
|
||||||
setVideoField,
|
setVideoField,
|
||||||
setPlaylistField,
|
setPlaylistField,
|
||||||
setActorField,
|
setActorField,
|
||||||
|
countVideoViewsOf,
|
||||||
closeAllSequelize
|
closeAllSequelize
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue