]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add filter by start/end date overall stats in api
authorChocobozzz <me@florianbigard.com>
Thu, 5 May 2022 12:12:57 +0000 (14:12 +0200)
committerChocobozzz <me@florianbigard.com>
Thu, 5 May 2022 12:13:14 +0000 (14:13 +0200)
server/controllers/api/videos/stats.ts
server/middlewares/validators/videos/video-stats.ts
server/models/view/local-video-viewer.ts
server/tests/api/check-params/views.ts
server/tests/api/views/video-views-overall-stats.ts
shared/models/videos/stats/index.ts
shared/models/videos/stats/video-stats-overall-query.model.ts [new file with mode: 0644]
shared/server-commands/videos/video-stats-command.ts
support/doc/api/openapi.yaml

index 71452d9f00156f74d123056e68e8ce3285bc885e..30e2bb06c347f7295a0aeb221c04863b7c49a9e0 100644 (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)
 }
index 12509abdecc8cbf6db80014c94b9ce2a5ca9dfee..f17fbcc0953a2fe83c5628909d765138a7e20105 100644 (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 })
 
index 5928ba5f6fcf64b69d0175a9d9e30f68d60f354a..2862f8b96454651821b8f9fb1621d1eaf7d06357 100644 (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,
index 3dba2a42e59dbb151eac3118a0fbca491698d53c..fe037b1455ba709cda296e229c61dbcaf1c798e3 100644 (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()
+      })
     })
   })
 
index 72b072c963dd31e2382aea79a8d20f19e4c08293..53b8f0d4b97e91e570ae6819e6ac6a1ac0e9e5c6 100644 (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)
     })
index 4a6fdaa717e0e62a8a387853b224a31eee247459..a9b203f5861f2b00c7329e0b2becdd5555a3be9e 100644 (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'
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 (file)
index 0000000..6b4c216
--- /dev/null
@@ -0,0 +1,4 @@
+export interface VideoStatsOverallQuery {
+  startDate?: string
+  endDate?: string
+}
index bd4808f63dc12e8501eab317baef4835c499c746..b9b99bfb57ec0f9e2a9f140439e71e1ff5e8f081 100644 (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
     })
index 95670925f8ef867aa54538cab5277d1e406298ec..294aa50abe0d967d5a1dedfa07eacbf73386e635 100644 (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