aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--server/controllers/api/videos/stats.ts9
-rw-r--r--server/middlewares/validators/videos/video-stats.ts10
-rw-r--r--server/models/view/local-video-viewer.ts36
-rw-r--r--server/tests/api/check-params/views.ts24
-rw-r--r--server/tests/api/views/video-views-overall-stats.ts21
-rw-r--r--shared/models/videos/stats/index.ts1
-rw-r--r--shared/models/videos/stats/video-stats-overall-query.model.ts4
-rw-r--r--shared/server-commands/videos/video-stats-command.ts4
-rw-r--r--support/doc/api/openapi.yaml24
9 files changed, 121 insertions, 12 deletions
diff --git a/server/controllers/api/videos/stats.ts b/server/controllers/api/videos/stats.ts
index 71452d9f0..30e2bb06c 100644
--- a/server/controllers/api/videos/stats.ts
+++ b/server/controllers/api/videos/stats.ts
@@ -1,6 +1,6 @@
1import express from 'express' 1import express from 'express'
2import { LocalVideoViewerModel } from '@server/models/view/local-video-viewer' 2import { LocalVideoViewerModel } from '@server/models/view/local-video-viewer'
3import { VideoStatsTimeserieMetric, VideoStatsTimeserieQuery } from '@shared/models' 3import { VideoStatsOverallQuery, VideoStatsTimeserieMetric, VideoStatsTimeserieQuery } from '@shared/models'
4import { 4import {
5 asyncMiddleware, 5 asyncMiddleware,
6 authenticate, 6 authenticate,
@@ -39,8 +39,13 @@ export {
39 39
40async function getOverallStats (req: express.Request, res: express.Response) { 40async function getOverallStats (req: express.Request, res: express.Response) {
41 const video = res.locals.videoAll 41 const video = res.locals.videoAll
42 const query = req.query as VideoStatsOverallQuery
42 43
43 const stats = await LocalVideoViewerModel.getOverallStats(video) 44 const stats = await LocalVideoViewerModel.getOverallStats({
45 video,
46 startDate: query.startDate,
47 endDate: query.endDate
48 })
44 49
45 return res.json(stats) 50 return res.json(stats)
46} 51}
diff --git a/server/middlewares/validators/videos/video-stats.ts b/server/middlewares/validators/videos/video-stats.ts
index 12509abde..f17fbcc09 100644
--- a/server/middlewares/validators/videos/video-stats.ts
+++ b/server/middlewares/validators/videos/video-stats.ts
@@ -10,6 +10,16 @@ import { areValidationErrors, checkUserCanManageVideo, doesVideoExist, isValidVi
10const videoOverallStatsValidator = [ 10const videoOverallStatsValidator = [
11 isValidVideoIdParam('videoId'), 11 isValidVideoIdParam('videoId'),
12 12
13 query('startDate')
14 .optional()
15 .custom(isDateValid)
16 .withMessage('Should have a valid start date'),
17
18 query('endDate')
19 .optional()
20 .custom(isDateValid)
21 .withMessage('Should have a valid end date'),
22
13 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 23 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
14 logger.debug('Checking videoOverallStatsValidator parameters', { parameters: req.body }) 24 logger.debug('Checking videoOverallStatsValidator parameters', { parameters: req.body })
15 25
diff --git a/server/models/view/local-video-viewer.ts b/server/models/view/local-video-viewer.ts
index 5928ba5f6..2862f8b96 100644
--- a/server/models/view/local-video-viewer.ts
+++ b/server/models/view/local-video-viewer.ts
@@ -100,10 +100,28 @@ export class LocalVideoViewerModel extends Model<Partial<AttributesOnly<LocalVid
100 }) 100 })
101 } 101 }
102 102
103 static async getOverallStats (video: MVideo): Promise<VideoStatsOverall> { 103 static async getOverallStats (options: {
104 const options = { 104 video: MVideo
105 startDate?: string
106 endDate?: string
107 }): Promise<VideoStatsOverall> {
108 const { video, startDate, endDate } = options
109
110 const queryOptions = {
105 type: QueryTypes.SELECT as QueryTypes.SELECT, 111 type: QueryTypes.SELECT as QueryTypes.SELECT,
106 replacements: { videoId: video.id } 112 replacements: { videoId: video.id } as any
113 }
114
115 let dateWhere = ''
116
117 if (startDate) {
118 dateWhere += ' AND "localVideoViewer"."startDate" >= :startDate'
119 queryOptions.replacements.startDate = startDate
120 }
121
122 if (endDate) {
123 dateWhere += ' AND "localVideoViewer"."endDate" <= :endDate'
124 queryOptions.replacements.endDate = endDate
107 } 125 }
108 126
109 const watchTimeQuery = `SELECT ` + 127 const watchTimeQuery = `SELECT ` +
@@ -111,9 +129,9 @@ export class LocalVideoViewerModel extends Model<Partial<AttributesOnly<LocalVid
111 `AVG("localVideoViewer"."watchTime") AS "averageWatchTime" ` + 129 `AVG("localVideoViewer"."watchTime") AS "averageWatchTime" ` +
112 `FROM "localVideoViewer" ` + 130 `FROM "localVideoViewer" ` +
113 `INNER JOIN "video" ON "video"."id" = "localVideoViewer"."videoId" ` + 131 `INNER JOIN "video" ON "video"."id" = "localVideoViewer"."videoId" ` +
114 `WHERE "videoId" = :videoId` 132 `WHERE "videoId" = :videoId ${dateWhere}`
115 133
116 const watchTimePromise = LocalVideoViewerModel.sequelize.query<any>(watchTimeQuery, options) 134 const watchTimePromise = LocalVideoViewerModel.sequelize.query<any>(watchTimeQuery, queryOptions)
117 135
118 const watchPeakQuery = `WITH "watchPeakValues" AS ( 136 const watchPeakQuery = `WITH "watchPeakValues" AS (
119 SELECT "startDate" AS "dateBreakpoint", 1 AS "inc" 137 SELECT "startDate" AS "dateBreakpoint", 1 AS "inc"
@@ -122,7 +140,7 @@ export class LocalVideoViewerModel extends Model<Partial<AttributesOnly<LocalVid
122 UNION ALL 140 UNION ALL
123 SELECT "endDate" AS "dateBreakpoint", -1 AS "inc" 141 SELECT "endDate" AS "dateBreakpoint", -1 AS "inc"
124 FROM "localVideoViewer" 142 FROM "localVideoViewer"
125 WHERE "videoId" = :videoId 143 WHERE "videoId" = :videoId ${dateWhere}
126 ) 144 )
127 SELECT "dateBreakpoint", "concurrent" 145 SELECT "dateBreakpoint", "concurrent"
128 FROM ( 146 FROM (
@@ -132,14 +150,14 @@ export class LocalVideoViewerModel extends Model<Partial<AttributesOnly<LocalVid
132 ) tmp 150 ) tmp
133 ORDER BY "concurrent" DESC 151 ORDER BY "concurrent" DESC
134 FETCH FIRST 1 ROW ONLY` 152 FETCH FIRST 1 ROW ONLY`
135 const watchPeakPromise = LocalVideoViewerModel.sequelize.query<any>(watchPeakQuery, options) 153 const watchPeakPromise = LocalVideoViewerModel.sequelize.query<any>(watchPeakQuery, queryOptions)
136 154
137 const countriesQuery = `SELECT country, COUNT(country) as viewers ` + 155 const countriesQuery = `SELECT country, COUNT(country) as viewers ` +
138 `FROM "localVideoViewer" ` + 156 `FROM "localVideoViewer" ` +
139 `WHERE "videoId" = :videoId AND country IS NOT NULL ` + 157 `WHERE "videoId" = :videoId AND country IS NOT NULL ${dateWhere} ` +
140 `GROUP BY country ` + 158 `GROUP BY country ` +
141 `ORDER BY viewers DESC` 159 `ORDER BY viewers DESC`
142 const countriesPromise = LocalVideoViewerModel.sequelize.query<any>(countriesQuery, options) 160 const countriesPromise = LocalVideoViewerModel.sequelize.query<any>(countriesQuery, queryOptions)
143 161
144 const [ rowsWatchTime, rowsWatchPeak, rowsCountries ] = await Promise.all([ 162 const [ rowsWatchTime, rowsWatchPeak, rowsCountries ] = await Promise.all([
145 watchTimePromise, 163 watchTimePromise,
diff --git a/server/tests/api/check-params/views.ts b/server/tests/api/check-params/views.ts
index 3dba2a42e..fe037b145 100644
--- a/server/tests/api/check-params/views.ts
+++ b/server/tests/api/check-params/views.ts
@@ -75,8 +75,30 @@ describe('Test videos views', function () {
75 }) 75 })
76 }) 76 })
77 77
78 it('Should fail with an invalid start date', async function () {
79 await servers[0].videoStats.getOverallStats({
80 videoId,
81 startDate: 'fake' as any,
82 endDate: new Date().toISOString(),
83 expectedStatus: HttpStatusCode.BAD_REQUEST_400
84 })
85 })
86
87 it('Should fail with an invalid end date', async function () {
88 await servers[0].videoStats.getOverallStats({
89 videoId,
90 startDate: new Date().toISOString(),
91 endDate: 'fake' as any,
92 expectedStatus: HttpStatusCode.BAD_REQUEST_400
93 })
94 })
95
78 it('Should succeed with the correct parameters', async function () { 96 it('Should succeed with the correct parameters', async function () {
79 await servers[0].videoStats.getOverallStats({ videoId }) 97 await servers[0].videoStats.getOverallStats({
98 videoId,
99 startDate: new Date().toISOString(),
100 endDate: new Date().toISOString()
101 })
80 }) 102 })
81 }) 103 })
82 104
diff --git a/server/tests/api/views/video-views-overall-stats.ts b/server/tests/api/views/video-views-overall-stats.ts
index 72b072c96..53b8f0d4b 100644
--- a/server/tests/api/views/video-views-overall-stats.ts
+++ b/server/tests/api/views/video-views-overall-stats.ts
@@ -141,6 +141,27 @@ describe('Test views overall stats', function () {
141 } 141 }
142 }) 142 })
143 143
144 it('Should filter overall stats by date', async function () {
145 this.timeout(60000)
146
147 const beforeView = new Date()
148
149 await servers[0].views.simulateViewer({ id: vodVideoId, currentTimes: [ 0, 3 ] })
150 await processViewersStats(servers)
151
152 {
153 const stats = await servers[0].videoStats.getOverallStats({ videoId: vodVideoId, startDate: beforeView.toISOString() })
154 expect(stats.averageWatchTime).to.equal(3)
155 expect(stats.totalWatchTime).to.equal(3)
156 }
157
158 {
159 const stats = await servers[0].videoStats.getOverallStats({ videoId: liveVideoId, endDate: beforeView.toISOString() })
160 expect(stats.averageWatchTime).to.equal(22)
161 expect(stats.totalWatchTime).to.equal(88)
162 }
163 })
164
144 after(async function () { 165 after(async function () {
145 await stopFfmpeg(command) 166 await stopFfmpeg(command)
146 }) 167 })
diff --git a/shared/models/videos/stats/index.ts b/shared/models/videos/stats/index.ts
index 4a6fdaa71..a9b203f58 100644
--- a/shared/models/videos/stats/index.ts
+++ b/shared/models/videos/stats/index.ts
@@ -1,3 +1,4 @@
1export * from './video-stats-overall-query.model'
1export * from './video-stats-overall.model' 2export * from './video-stats-overall.model'
2export * from './video-stats-retention.model' 3export * from './video-stats-retention.model'
3export * from './video-stats-timeserie-query.model' 4export * from './video-stats-timeserie-query.model'
diff --git a/shared/models/videos/stats/video-stats-overall-query.model.ts b/shared/models/videos/stats/video-stats-overall-query.model.ts
new file mode 100644
index 000000000..6b4c2164f
--- /dev/null
+++ b/shared/models/videos/stats/video-stats-overall-query.model.ts
@@ -0,0 +1,4 @@
1export interface VideoStatsOverallQuery {
2 startDate?: string
3 endDate?: string
4}
diff --git a/shared/server-commands/videos/video-stats-command.ts b/shared/server-commands/videos/video-stats-command.ts
index bd4808f63..b9b99bfb5 100644
--- a/shared/server-commands/videos/video-stats-command.ts
+++ b/shared/server-commands/videos/video-stats-command.ts
@@ -6,6 +6,8 @@ export class VideoStatsCommand extends AbstractCommand {
6 6
7 getOverallStats (options: OverrideCommandOptions & { 7 getOverallStats (options: OverrideCommandOptions & {
8 videoId: number | string 8 videoId: number | string
9 startDate?: string
10 endDate?: string
9 }) { 11 }) {
10 const path = '/api/v1/videos/' + options.videoId + '/stats/overall' 12 const path = '/api/v1/videos/' + options.videoId + '/stats/overall'
11 13
@@ -13,6 +15,8 @@ export class VideoStatsCommand extends AbstractCommand {
13 ...options, 15 ...options,
14 path, 16 path,
15 17
18 query: pick(options, [ 'startDate', 'endDate' ]),
19
16 implicitToken: true, 20 implicitToken: true,
17 defaultExpectedStatus: HttpStatusCode.OK_200 21 defaultExpectedStatus: HttpStatusCode.OK_200
18 }) 22 })
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml
index 95670925f..294aa50ab 100644
--- a/support/doc/api/openapi.yaml
+++ b/support/doc/api/openapi.yaml
@@ -1952,6 +1952,18 @@ paths:
1952 - OAuth2: [] 1952 - OAuth2: []
1953 parameters: 1953 parameters:
1954 - $ref: '#/components/parameters/idOrUUID' 1954 - $ref: '#/components/parameters/idOrUUID'
1955 - name: startDate
1956 in: query
1957 description: Filter stats by start date
1958 schema:
1959 type: string
1960 format: date-time
1961 - name: endDate
1962 in: query
1963 description: Filter stats by end date
1964 schema:
1965 type: string
1966 format: date-time
1955 responses: 1967 responses:
1956 '200': 1968 '200':
1957 description: successful operation 1969 description: successful operation
@@ -1996,6 +2008,18 @@ paths:
1996 enum: 2008 enum:
1997 - 'viewers' 2009 - 'viewers'
1998 - 'aggregateWatchTime' 2010 - 'aggregateWatchTime'
2011 - name: startDate
2012 in: query
2013 description: Filter stats by start date
2014 schema:
2015 type: string
2016 format: date-time
2017 - name: endDate
2018 in: query
2019 description: Filter stats by end date
2020 schema:
2021 type: string
2022 format: date-time
1999 responses: 2023 responses:
2000 '200': 2024 '200':
2001 description: successful operation 2025 description: successful operation