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,
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)
}
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 })
})
}
- 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 ` +
`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"
UNION ALL
SELECT "endDate" AS "dateBreakpoint", -1 AS "inc"
FROM "localVideoViewer"
- WHERE "videoId" = :videoId
+ WHERE "videoId" = :videoId ${dateWhere}
)
SELECT "dateBreakpoint", "concurrent"
FROM (
) 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,
})
})
+ 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()
+ })
})
})
}
})
+ 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)
})
+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'
--- /dev/null
+export interface VideoStatsOverallQuery {
+ startDate?: string
+ endDate?: string
+}
getOverallStats (options: OverrideCommandOptions & {
videoId: number | string
+ startDate?: string
+ endDate?: string
}) {
const path = '/api/v1/videos/' + options.videoId + '/stats/overall'
...options,
path,
+ query: pick(options, [ 'startDate', 'endDate' ]),
+
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.OK_200
})
- 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
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