Add filter by start/end date overall stats in api
This commit is contained in:
parent
f18a060a83
commit
49f0468d44
|
@ -1,6 +1,6 @@
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { LocalVideoViewerModel } from '@server/models/view/local-video-viewer'
|
import { LocalVideoViewerModel } from '@server/models/view/local-video-viewer'
|
||||||
import { VideoStatsTimeserieMetric, VideoStatsTimeserieQuery } from '@shared/models'
|
import { VideoStatsOverallQuery, VideoStatsTimeserieMetric, VideoStatsTimeserieQuery } from '@shared/models'
|
||||||
import {
|
import {
|
||||||
asyncMiddleware,
|
asyncMiddleware,
|
||||||
authenticate,
|
authenticate,
|
||||||
|
@ -39,8 +39,13 @@ export {
|
||||||
|
|
||||||
async function getOverallStats (req: express.Request, res: express.Response) {
|
async function getOverallStats (req: express.Request, res: express.Response) {
|
||||||
const video = res.locals.videoAll
|
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)
|
return res.json(stats)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,16 @@ import { areValidationErrors, checkUserCanManageVideo, doesVideoExist, isValidVi
|
||||||
const videoOverallStatsValidator = [
|
const videoOverallStatsValidator = [
|
||||||
isValidVideoIdParam('videoId'),
|
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) => {
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
logger.debug('Checking videoOverallStatsValidator parameters', { parameters: req.body })
|
logger.debug('Checking videoOverallStatsValidator parameters', { parameters: req.body })
|
||||||
|
|
||||||
|
|
|
@ -100,10 +100,28 @@ export class LocalVideoViewerModel extends Model<Partial<AttributesOnly<LocalVid
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getOverallStats (video: MVideo): Promise<VideoStatsOverall> {
|
static async getOverallStats (options: {
|
||||||
const options = {
|
video: MVideo
|
||||||
|
startDate?: string
|
||||||
|
endDate?: string
|
||||||
|
}): Promise<VideoStatsOverall> {
|
||||||
|
const { video, startDate, endDate } = options
|
||||||
|
|
||||||
|
const queryOptions = {
|
||||||
type: QueryTypes.SELECT as QueryTypes.SELECT,
|
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 ` +
|
const watchTimeQuery = `SELECT ` +
|
||||||
|
@ -111,9 +129,9 @@ export class LocalVideoViewerModel extends Model<Partial<AttributesOnly<LocalVid
|
||||||
`AVG("localVideoViewer"."watchTime") AS "averageWatchTime" ` +
|
`AVG("localVideoViewer"."watchTime") AS "averageWatchTime" ` +
|
||||||
`FROM "localVideoViewer" ` +
|
`FROM "localVideoViewer" ` +
|
||||||
`INNER JOIN "video" ON "video"."id" = "localVideoViewer"."videoId" ` +
|
`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 (
|
const watchPeakQuery = `WITH "watchPeakValues" AS (
|
||||||
SELECT "startDate" AS "dateBreakpoint", 1 AS "inc"
|
SELECT "startDate" AS "dateBreakpoint", 1 AS "inc"
|
||||||
|
@ -122,7 +140,7 @@ export class LocalVideoViewerModel extends Model<Partial<AttributesOnly<LocalVid
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT "endDate" AS "dateBreakpoint", -1 AS "inc"
|
SELECT "endDate" AS "dateBreakpoint", -1 AS "inc"
|
||||||
FROM "localVideoViewer"
|
FROM "localVideoViewer"
|
||||||
WHERE "videoId" = :videoId
|
WHERE "videoId" = :videoId ${dateWhere}
|
||||||
)
|
)
|
||||||
SELECT "dateBreakpoint", "concurrent"
|
SELECT "dateBreakpoint", "concurrent"
|
||||||
FROM (
|
FROM (
|
||||||
|
@ -132,14 +150,14 @@ export class LocalVideoViewerModel extends Model<Partial<AttributesOnly<LocalVid
|
||||||
) tmp
|
) tmp
|
||||||
ORDER BY "concurrent" DESC
|
ORDER BY "concurrent" DESC
|
||||||
FETCH FIRST 1 ROW ONLY`
|
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 ` +
|
const countriesQuery = `SELECT country, COUNT(country) as viewers ` +
|
||||||
`FROM "localVideoViewer" ` +
|
`FROM "localVideoViewer" ` +
|
||||||
`WHERE "videoId" = :videoId AND country IS NOT NULL ` +
|
`WHERE "videoId" = :videoId AND country IS NOT NULL ${dateWhere} ` +
|
||||||
`GROUP BY country ` +
|
`GROUP BY country ` +
|
||||||
`ORDER BY viewers DESC`
|
`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([
|
const [ rowsWatchTime, rowsWatchPeak, rowsCountries ] = await Promise.all([
|
||||||
watchTimePromise,
|
watchTimePromise,
|
||||||
|
|
|
@ -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 () {
|
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()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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 () {
|
after(async function () {
|
||||||
await stopFfmpeg(command)
|
await stopFfmpeg(command)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
export * from './video-stats-overall-query.model'
|
||||||
export * from './video-stats-overall.model'
|
export * from './video-stats-overall.model'
|
||||||
export * from './video-stats-retention.model'
|
export * from './video-stats-retention.model'
|
||||||
export * from './video-stats-timeserie-query.model'
|
export * from './video-stats-timeserie-query.model'
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
export interface VideoStatsOverallQuery {
|
||||||
|
startDate?: string
|
||||||
|
endDate?: string
|
||||||
|
}
|
|
@ -6,6 +6,8 @@ export class VideoStatsCommand extends AbstractCommand {
|
||||||
|
|
||||||
getOverallStats (options: OverrideCommandOptions & {
|
getOverallStats (options: OverrideCommandOptions & {
|
||||||
videoId: number | string
|
videoId: number | string
|
||||||
|
startDate?: string
|
||||||
|
endDate?: string
|
||||||
}) {
|
}) {
|
||||||
const path = '/api/v1/videos/' + options.videoId + '/stats/overall'
|
const path = '/api/v1/videos/' + options.videoId + '/stats/overall'
|
||||||
|
|
||||||
|
@ -13,6 +15,8 @@ export class VideoStatsCommand extends AbstractCommand {
|
||||||
...options,
|
...options,
|
||||||
path,
|
path,
|
||||||
|
|
||||||
|
query: pick(options, [ 'startDate', 'endDate' ]),
|
||||||
|
|
||||||
implicitToken: true,
|
implicitToken: true,
|
||||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||||
})
|
})
|
||||||
|
|
|
@ -1952,6 +1952,18 @@ paths:
|
||||||
- OAuth2: []
|
- OAuth2: []
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/components/parameters/idOrUUID'
|
- $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:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: successful operation
|
description: successful operation
|
||||||
|
@ -1996,6 +2008,18 @@ paths:
|
||||||
enum:
|
enum:
|
||||||
- 'viewers'
|
- 'viewers'
|
||||||
- 'aggregateWatchTime'
|
- '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:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: successful operation
|
description: successful operation
|
||||||
|
|
Loading…
Reference in New Issue
Block a user