Add filter by start/end date overall stats in api

pull/4977/head
Chocobozzz 2022-05-05 14:12:57 +02:00
parent f18a060a83
commit 49f0468d44
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
9 changed files with 121 additions and 12 deletions

View File

@ -1,6 +1,6 @@
import express from 'express'
import { LocalVideoViewerModel } from '@server/models/view/local-video-viewer'
import { VideoStatsTimeserieMetric, VideoStatsTimeserieQuery } from '@shared/models'
import { VideoStatsOverallQuery, VideoStatsTimeserieMetric, VideoStatsTimeserieQuery } from '@shared/models'
import {
asyncMiddleware,
authenticate,
@ -39,8 +39,13 @@ export {
async function getOverallStats (req: express.Request, res: express.Response) {
const video = res.locals.videoAll
const query = req.query as VideoStatsOverallQuery
const stats = await LocalVideoViewerModel.getOverallStats(video)
const stats = await LocalVideoViewerModel.getOverallStats({
video,
startDate: query.startDate,
endDate: query.endDate
})
return res.json(stats)
}

View File

@ -10,6 +10,16 @@ import { areValidationErrors, checkUserCanManageVideo, doesVideoExist, isValidVi
const videoOverallStatsValidator = [
isValidVideoIdParam('videoId'),
query('startDate')
.optional()
.custom(isDateValid)
.withMessage('Should have a valid start date'),
query('endDate')
.optional()
.custom(isDateValid)
.withMessage('Should have a valid end date'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoOverallStatsValidator parameters', { parameters: req.body })

View File

@ -100,10 +100,28 @@ export class LocalVideoViewerModel extends Model<Partial<AttributesOnly<LocalVid
})
}
static async getOverallStats (video: MVideo): Promise<VideoStatsOverall> {
const options = {
static async getOverallStats (options: {
video: MVideo
startDate?: string
endDate?: string
}): Promise<VideoStatsOverall> {
const { video, startDate, endDate } = options
const queryOptions = {
type: QueryTypes.SELECT as QueryTypes.SELECT,
replacements: { videoId: video.id }
replacements: { videoId: video.id } as any
}
let dateWhere = ''
if (startDate) {
dateWhere += ' AND "localVideoViewer"."startDate" >= :startDate'
queryOptions.replacements.startDate = startDate
}
if (endDate) {
dateWhere += ' AND "localVideoViewer"."endDate" <= :endDate'
queryOptions.replacements.endDate = endDate
}
const watchTimeQuery = `SELECT ` +
@ -111,9 +129,9 @@ export class LocalVideoViewerModel extends Model<Partial<AttributesOnly<LocalVid
`AVG("localVideoViewer"."watchTime") AS "averageWatchTime" ` +
`FROM "localVideoViewer" ` +
`INNER JOIN "video" ON "video"."id" = "localVideoViewer"."videoId" ` +
`WHERE "videoId" = :videoId`
`WHERE "videoId" = :videoId ${dateWhere}`
const watchTimePromise = LocalVideoViewerModel.sequelize.query<any>(watchTimeQuery, options)
const watchTimePromise = LocalVideoViewerModel.sequelize.query<any>(watchTimeQuery, queryOptions)
const watchPeakQuery = `WITH "watchPeakValues" AS (
SELECT "startDate" AS "dateBreakpoint", 1 AS "inc"
@ -122,7 +140,7 @@ export class LocalVideoViewerModel extends Model<Partial<AttributesOnly<LocalVid
UNION ALL
SELECT "endDate" AS "dateBreakpoint", -1 AS "inc"
FROM "localVideoViewer"
WHERE "videoId" = :videoId
WHERE "videoId" = :videoId ${dateWhere}
)
SELECT "dateBreakpoint", "concurrent"
FROM (
@ -132,14 +150,14 @@ export class LocalVideoViewerModel extends Model<Partial<AttributesOnly<LocalVid
) tmp
ORDER BY "concurrent" DESC
FETCH FIRST 1 ROW ONLY`
const watchPeakPromise = LocalVideoViewerModel.sequelize.query<any>(watchPeakQuery, options)
const watchPeakPromise = LocalVideoViewerModel.sequelize.query<any>(watchPeakQuery, queryOptions)
const countriesQuery = `SELECT country, COUNT(country) as viewers ` +
`FROM "localVideoViewer" ` +
`WHERE "videoId" = :videoId AND country IS NOT NULL ` +
`WHERE "videoId" = :videoId AND country IS NOT NULL ${dateWhere} ` +
`GROUP BY country ` +
`ORDER BY viewers DESC`
const countriesPromise = LocalVideoViewerModel.sequelize.query<any>(countriesQuery, options)
const countriesPromise = LocalVideoViewerModel.sequelize.query<any>(countriesQuery, queryOptions)
const [ rowsWatchTime, rowsWatchPeak, rowsCountries ] = await Promise.all([
watchTimePromise,

View File

@ -75,8 +75,30 @@ describe('Test videos views', function () {
})
})
it('Should fail with an invalid start date', async function () {
await servers[0].videoStats.getOverallStats({
videoId,
startDate: 'fake' as any,
endDate: new Date().toISOString(),
expectedStatus: HttpStatusCode.BAD_REQUEST_400
})
})
it('Should fail with an invalid end date', async function () {
await servers[0].videoStats.getOverallStats({
videoId,
startDate: new Date().toISOString(),
endDate: 'fake' as any,
expectedStatus: HttpStatusCode.BAD_REQUEST_400
})
})
it('Should succeed with the correct parameters', async function () {
await servers[0].videoStats.getOverallStats({ videoId })
await servers[0].videoStats.getOverallStats({
videoId,
startDate: new Date().toISOString(),
endDate: new Date().toISOString()
})
})
})

View File

@ -141,6 +141,27 @@ describe('Test views overall stats', function () {
}
})
it('Should filter overall stats by date', async function () {
this.timeout(60000)
const beforeView = new Date()
await servers[0].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 3 ] })
await processViewersStats(servers)
{
const stats = await servers[0].videoStats.getOverallStats({ videoId: vodVideoId, startDate: beforeView.toISOString() })
expect(stats.averageWatchTime).to.equal(3)
expect(stats.totalWatchTime).to.equal(3)
}
{
const stats = await servers[0].videoStats.getOverallStats({ videoId: liveVideoId, endDate: beforeView.toISOString() })
expect(stats.averageWatchTime).to.equal(22)
expect(stats.totalWatchTime).to.equal(88)
}
})
after(async function () {
await stopFfmpeg(command)
})

View File

@ -1,3 +1,4 @@
export * from './video-stats-overall-query.model'
export * from './video-stats-overall.model'
export * from './video-stats-retention.model'
export * from './video-stats-timeserie-query.model'

View File

@ -0,0 +1,4 @@
export interface VideoStatsOverallQuery {
startDate?: string
endDate?: string
}

View File

@ -6,6 +6,8 @@ export class VideoStatsCommand extends AbstractCommand {
getOverallStats (options: OverrideCommandOptions & {
videoId: number | string
startDate?: string
endDate?: string
}) {
const path = '/api/v1/videos/' + options.videoId + '/stats/overall'
@ -13,6 +15,8 @@ export class VideoStatsCommand extends AbstractCommand {
...options,
path,
query: pick(options, [ 'startDate', 'endDate' ]),
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.OK_200
})

View File

@ -1952,6 +1952,18 @@ paths:
- OAuth2: []
parameters:
- $ref: '#/components/parameters/idOrUUID'
- name: startDate
in: query
description: Filter stats by start date
schema:
type: string
format: date-time
- name: endDate
in: query
description: Filter stats by end date
schema:
type: string
format: date-time
responses:
'200':
description: successful operation
@ -1996,6 +2008,18 @@ paths:
enum:
- 'viewers'
- 'aggregateWatchTime'
- name: startDate
in: query
description: Filter stats by start date
schema:
type: string
format: date-time
- name: endDate
in: query
description: Filter stats by end date
schema:
type: string
format: date-time
responses:
'200':
description: successful operation