aboutsummaryrefslogtreecommitdiffhomepage
path: root/shared
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-03-24 13:36:47 +0100
committerChocobozzz <chocobozzz@cpy.re>2022-04-15 09:49:35 +0200
commitb211106695bb82f6c32e53306081b5262c3d109d (patch)
treefa187de1c33b0956665f5362e29af6b0f6d8bb57 /shared
parent69d48ee30c9d47cddf0c3c047dc99a99dcb6e894 (diff)
downloadPeerTube-b211106695bb82f6c32e53306081b5262c3d109d.tar.gz
PeerTube-b211106695bb82f6c32e53306081b5262c3d109d.tar.zst
PeerTube-b211106695bb82f6c32e53306081b5262c3d109d.zip
Support video views/viewers stats in server
* Add "currentTime" and "event" body params to view endpoint * Merge watching and view endpoints * Introduce WatchAction AP activity * Add tables to store viewer information of local videos * Add endpoints to fetch video views/viewers stats of local videos * Refactor views/viewers handlers * Support "views" and "viewers" counters for both VOD and live videos
Diffstat (limited to 'shared')
-rw-r--r--shared/models/activitypub/activity.ts8
-rw-r--r--shared/models/activitypub/context.ts3
-rw-r--r--shared/models/activitypub/objects/index.ts1
-rw-r--r--shared/models/activitypub/objects/watch-action-object.ts22
-rw-r--r--shared/models/server/debug.model.ts2
-rw-r--r--shared/models/users/index.ts1
-rw-r--r--shared/models/users/user-watching-video.model.ts3
-rw-r--r--shared/models/videos/index.ts2
-rw-r--r--shared/models/videos/stats/index.ts4
-rw-r--r--shared/models/videos/stats/video-stats-overall.model.ts17
-rw-r--r--shared/models/videos/stats/video-stats-retention.model.ts6
-rw-r--r--shared/models/videos/stats/video-stats-timeserie-metric.type.ts1
-rw-r--r--shared/models/videos/stats/video-stats-timeserie.model.ts6
-rw-r--r--shared/models/videos/video-view.model.ts6
-rw-r--r--shared/models/videos/video.model.ts3
-rw-r--r--shared/server-commands/server/server.ts8
-rw-r--r--shared/server-commands/videos/history-command.ts19
-rw-r--r--shared/server-commands/videos/index.ts1
-rw-r--r--shared/server-commands/videos/video-stats-command.ts48
-rw-r--r--shared/server-commands/videos/videos-command.ts17
-rw-r--r--shared/server-commands/videos/views-command.ts51
21 files changed, 181 insertions, 48 deletions
diff --git a/shared/models/activitypub/activity.ts b/shared/models/activitypub/activity.ts
index d6284e283..fd5d38316 100644
--- a/shared/models/activitypub/activity.ts
+++ b/shared/models/activitypub/activity.ts
@@ -1,6 +1,6 @@
1import { ActivityPubActor } from './activitypub-actor' 1import { ActivityPubActor } from './activitypub-actor'
2import { ActivityPubSignature } from './activitypub-signature' 2import { ActivityPubSignature } from './activitypub-signature'
3import { ActivityFlagReasonObject, CacheFileObject, VideoObject } from './objects' 3import { ActivityFlagReasonObject, CacheFileObject, VideoObject, WatchActionObject } from './objects'
4import { AbuseObject } from './objects/abuse-object' 4import { AbuseObject } from './objects/abuse-object'
5import { DislikeObject } from './objects/dislike-object' 5import { DislikeObject } from './objects/dislike-object'
6import { APObject } from './objects/object.model' 6import { APObject } from './objects/object.model'
@@ -52,7 +52,7 @@ export interface BaseActivity {
52 52
53export interface ActivityCreate extends BaseActivity { 53export interface ActivityCreate extends BaseActivity {
54 type: 'Create' 54 type: 'Create'
55 object: VideoObject | AbuseObject | DislikeObject | VideoCommentObject | CacheFileObject | PlaylistObject 55 object: VideoObject | AbuseObject | DislikeObject | VideoCommentObject | CacheFileObject | PlaylistObject | WatchActionObject
56} 56}
57 57
58export interface ActivityUpdate extends BaseActivity { 58export interface ActivityUpdate extends BaseActivity {
@@ -99,7 +99,9 @@ export interface ActivityView extends BaseActivity {
99 type: 'View' 99 type: 'View'
100 actor: string 100 actor: string
101 object: APObject 101 object: APObject
102 expires: string 102
103 // If sending a "viewer" event
104 expires?: string
103} 105}
104 106
105export interface ActivityDislike extends BaseActivity { 107export interface ActivityDislike extends BaseActivity {
diff --git a/shared/models/activitypub/context.ts b/shared/models/activitypub/context.ts
index 4ada3b083..e9df38207 100644
--- a/shared/models/activitypub/context.ts
+++ b/shared/models/activitypub/context.ts
@@ -12,4 +12,5 @@ export type ContextType =
12 'Rate' | 12 'Rate' |
13 'Flag' | 13 'Flag' |
14 'Actor' | 14 'Actor' |
15 'Collection' 15 'Collection' |
16 'WatchAction'
diff --git a/shared/models/activitypub/objects/index.ts b/shared/models/activitypub/objects/index.ts
index 9e2c6b728..47a8e847a 100644
--- a/shared/models/activitypub/objects/index.ts
+++ b/shared/models/activitypub/objects/index.ts
@@ -8,3 +8,4 @@ export * from './playlist-object'
8export * from './video-comment-object' 8export * from './video-comment-object'
9export * from './video-torrent-object' 9export * from './video-torrent-object'
10export * from './view-object' 10export * from './view-object'
11export * from './watch-action-object'
diff --git a/shared/models/activitypub/objects/watch-action-object.ts b/shared/models/activitypub/objects/watch-action-object.ts
new file mode 100644
index 000000000..ed336602f
--- /dev/null
+++ b/shared/models/activitypub/objects/watch-action-object.ts
@@ -0,0 +1,22 @@
1export interface WatchActionObject {
2 id: string
3 type: 'WatchAction'
4
5 startTime: string
6 endTime: string
7
8 location?: {
9 addressCountry: string
10 }
11
12 uuid: string
13 object: string
14 actionStatus: 'CompletedActionStatus'
15
16 duration: string
17
18 watchSections: {
19 startTimestamp: number
20 endTimestamp: number
21 }[]
22}
diff --git a/shared/models/server/debug.model.ts b/shared/models/server/debug.model.ts
index 2ecabdeca..223d23362 100644
--- a/shared/models/server/debug.model.ts
+++ b/shared/models/server/debug.model.ts
@@ -4,5 +4,5 @@ export interface Debug {
4} 4}
5 5
6export interface SendDebugCommand { 6export interface SendDebugCommand {
7 command: 'remove-dandling-resumable-uploads' 7 command: 'remove-dandling-resumable-uploads' | 'process-video-views-buffer' | 'process-video-viewers'
8} 8}
diff --git a/shared/models/users/index.ts b/shared/models/users/index.ts
index a24ffee96..b25978587 100644
--- a/shared/models/users/index.ts
+++ b/shared/models/users/index.ts
@@ -12,5 +12,4 @@ export * from './user-scoped-token'
12export * from './user-update-me.model' 12export * from './user-update-me.model'
13export * from './user-update.model' 13export * from './user-update.model'
14export * from './user-video-quota.model' 14export * from './user-video-quota.model'
15export * from './user-watching-video.model'
16export * from './user.model' 15export * from './user.model'
diff --git a/shared/models/users/user-watching-video.model.ts b/shared/models/users/user-watching-video.model.ts
deleted file mode 100644
index c22480595..000000000
--- a/shared/models/users/user-watching-video.model.ts
+++ /dev/null
@@ -1,3 +0,0 @@
1export interface UserWatchingVideo {
2 currentTime: number
3}
diff --git a/shared/models/videos/index.ts b/shared/models/videos/index.ts
index 705e8d0ff..05497bda1 100644
--- a/shared/models/videos/index.ts
+++ b/shared/models/videos/index.ts
@@ -9,6 +9,7 @@ export * from './file'
9export * from './import' 9export * from './import'
10export * from './playlist' 10export * from './playlist'
11export * from './rate' 11export * from './rate'
12export * from './stats'
12export * from './transcoding' 13export * from './transcoding'
13 14
14export * from './nsfw-policy.type' 15export * from './nsfw-policy.type'
@@ -32,5 +33,6 @@ export * from './video-streaming-playlist.model'
32export * from './video-streaming-playlist.type' 33export * from './video-streaming-playlist.type'
33 34
34export * from './video-update.model' 35export * from './video-update.model'
36export * from './video-view.model'
35export * from './video.model' 37export * from './video.model'
36export * from './video-create-result.model' 38export * from './video-create-result.model'
diff --git a/shared/models/videos/stats/index.ts b/shared/models/videos/stats/index.ts
new file mode 100644
index 000000000..d1e9c167c
--- /dev/null
+++ b/shared/models/videos/stats/index.ts
@@ -0,0 +1,4 @@
1export * from './video-stats-overall.model'
2export * from './video-stats-retention.model'
3export * from './video-stats-timeserie.model'
4export * from './video-stats-timeserie-metric.type'
diff --git a/shared/models/videos/stats/video-stats-overall.model.ts b/shared/models/videos/stats/video-stats-overall.model.ts
new file mode 100644
index 000000000..f2a0470ef
--- /dev/null
+++ b/shared/models/videos/stats/video-stats-overall.model.ts
@@ -0,0 +1,17 @@
1export interface VideoStatsOverall {
2 averageWatchTime: number
3 totalWatchTime: number
4
5 viewersPeak: number
6 viewersPeakDate: string
7
8 views: number
9 likes: number
10 dislikes: number
11 comments: number
12
13 countries: {
14 isoCode: string
15 viewers: number
16 }[]
17}
diff --git a/shared/models/videos/stats/video-stats-retention.model.ts b/shared/models/videos/stats/video-stats-retention.model.ts
new file mode 100644
index 000000000..e494888ed
--- /dev/null
+++ b/shared/models/videos/stats/video-stats-retention.model.ts
@@ -0,0 +1,6 @@
1export interface VideoStatsRetention {
2 data: {
3 second: number
4 retentionPercent: number
5 }[]
6}
diff --git a/shared/models/videos/stats/video-stats-timeserie-metric.type.ts b/shared/models/videos/stats/video-stats-timeserie-metric.type.ts
new file mode 100644
index 000000000..fc268d083
--- /dev/null
+++ b/shared/models/videos/stats/video-stats-timeserie-metric.type.ts
@@ -0,0 +1 @@
export type VideoStatsTimeserieMetric = 'viewers' | 'aggregateWatchTime'
diff --git a/shared/models/videos/stats/video-stats-timeserie.model.ts b/shared/models/videos/stats/video-stats-timeserie.model.ts
new file mode 100644
index 000000000..d95e34f1d
--- /dev/null
+++ b/shared/models/videos/stats/video-stats-timeserie.model.ts
@@ -0,0 +1,6 @@
1export interface VideoStatsTimeserie {
2 data: {
3 date: string
4 value: number
5 }[]
6}
diff --git a/shared/models/videos/video-view.model.ts b/shared/models/videos/video-view.model.ts
new file mode 100644
index 000000000..f61211104
--- /dev/null
+++ b/shared/models/videos/video-view.model.ts
@@ -0,0 +1,6 @@
1export type VideoViewEvent = 'seek'
2
3export interface VideoView {
4 currentTime: number
5 viewEvent?: VideoViewEvent
6}
diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts
index f98eed012..d9765dbd6 100644
--- a/shared/models/videos/video.model.ts
+++ b/shared/models/videos/video.model.ts
@@ -39,8 +39,7 @@ export interface Video {
39 url: string 39 url: string
40 40
41 views: number 41 views: number
42 // If live 42 viewers: number
43 viewers?: number
44 43
45 likes: number 44 likes: number
46 dislikes: number 45 dislikes: number
diff --git a/shared/server-commands/server/server.ts b/shared/server-commands/server/server.ts
index 2bf31b5a4..0ad818a11 100644
--- a/shared/server-commands/server/server.ts
+++ b/shared/server-commands/server/server.ts
@@ -25,10 +25,12 @@ import {
25 PlaylistsCommand, 25 PlaylistsCommand,
26 ServicesCommand, 26 ServicesCommand,
27 StreamingPlaylistsCommand, 27 StreamingPlaylistsCommand,
28 VideosCommand,
28 VideoStudioCommand, 29 VideoStudioCommand,
29 VideosCommand 30 ViewsCommand
30} from '../videos' 31} from '../videos'
31import { CommentsCommand } from '../videos/comments-command' 32import { CommentsCommand } from '../videos/comments-command'
33import { VideoStatsCommand } from '../videos/video-stats-command'
32import { ConfigCommand } from './config-command' 34import { ConfigCommand } from './config-command'
33import { ContactFormCommand } from './contact-form-command' 35import { ContactFormCommand } from './contact-form-command'
34import { DebugCommand } from './debug-command' 36import { DebugCommand } from './debug-command'
@@ -127,6 +129,8 @@ export class PeerTubeServer {
127 objectStorage?: ObjectStorageCommand 129 objectStorage?: ObjectStorageCommand
128 videoStudio?: VideoStudioCommand 130 videoStudio?: VideoStudioCommand
129 videos?: VideosCommand 131 videos?: VideosCommand
132 videoStats?: VideoStatsCommand
133 views?: ViewsCommand
130 134
131 constructor (options: { serverNumber: number } | { url: string }) { 135 constructor (options: { serverNumber: number } | { url: string }) {
132 if ((options as any).url) { 136 if ((options as any).url) {
@@ -397,5 +401,7 @@ export class PeerTubeServer {
397 this.videos = new VideosCommand(this) 401 this.videos = new VideosCommand(this)
398 this.objectStorage = new ObjectStorageCommand(this) 402 this.objectStorage = new ObjectStorageCommand(this)
399 this.videoStudio = new VideoStudioCommand(this) 403 this.videoStudio = new VideoStudioCommand(this)
404 this.videoStats = new VideoStatsCommand(this)
405 this.views = new ViewsCommand(this)
400 } 406 }
401} 407}
diff --git a/shared/server-commands/videos/history-command.ts b/shared/server-commands/videos/history-command.ts
index e9dc63462..d27afcff2 100644
--- a/shared/server-commands/videos/history-command.ts
+++ b/shared/server-commands/videos/history-command.ts
@@ -3,25 +3,6 @@ import { AbstractCommand, OverrideCommandOptions } from '../shared'
3 3
4export class HistoryCommand extends AbstractCommand { 4export class HistoryCommand extends AbstractCommand {
5 5
6 watchVideo (options: OverrideCommandOptions & {
7 videoId: number | string
8 currentTime: number
9 }) {
10 const { videoId, currentTime } = options
11
12 const path = '/api/v1/videos/' + videoId + '/watching'
13 const fields = { currentTime }
14
15 return this.putBodyRequest({
16 ...options,
17
18 path,
19 fields,
20 implicitToken: true,
21 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
22 })
23 }
24
25 list (options: OverrideCommandOptions & { 6 list (options: OverrideCommandOptions & {
26 search?: string 7 search?: string
27 } = {}) { 8 } = {}) {
diff --git a/shared/server-commands/videos/index.ts b/shared/server-commands/videos/index.ts
index c9ef6134d..b861731fb 100644
--- a/shared/server-commands/videos/index.ts
+++ b/shared/server-commands/videos/index.ts
@@ -13,4 +13,5 @@ export * from './services-command'
13export * from './streaming-playlists-command' 13export * from './streaming-playlists-command'
14export * from './comments-command' 14export * from './comments-command'
15export * from './video-studio-command' 15export * from './video-studio-command'
16export * from './views-command'
16export * from './videos-command' 17export * from './videos-command'
diff --git a/shared/server-commands/videos/video-stats-command.ts b/shared/server-commands/videos/video-stats-command.ts
new file mode 100644
index 000000000..90f7ffeaf
--- /dev/null
+++ b/shared/server-commands/videos/video-stats-command.ts
@@ -0,0 +1,48 @@
1import { HttpStatusCode, VideoStatsOverall, VideoStatsRetention, VideoStatsTimeserie, VideoStatsTimeserieMetric } from '@shared/models'
2import { AbstractCommand, OverrideCommandOptions } from '../shared'
3
4export class VideoStatsCommand extends AbstractCommand {
5
6 getOverallStats (options: OverrideCommandOptions & {
7 videoId: number | string
8 }) {
9 const path = '/api/v1/videos/' + options.videoId + '/stats/overall'
10
11 return this.getRequestBody<VideoStatsOverall>({
12 ...options,
13 path,
14
15 implicitToken: true,
16 defaultExpectedStatus: HttpStatusCode.OK_200
17 })
18 }
19
20 getTimeserieStats (options: OverrideCommandOptions & {
21 videoId: number | string
22 metric: VideoStatsTimeserieMetric
23 }) {
24 const path = '/api/v1/videos/' + options.videoId + '/stats/timeseries/' + options.metric
25
26 return this.getRequestBody<VideoStatsTimeserie>({
27 ...options,
28 path,
29
30 implicitToken: true,
31 defaultExpectedStatus: HttpStatusCode.OK_200
32 })
33 }
34
35 getRetentionStats (options: OverrideCommandOptions & {
36 videoId: number | string
37 }) {
38 const path = '/api/v1/videos/' + options.videoId + '/stats/retention'
39
40 return this.getRequestBody<VideoStatsRetention>({
41 ...options,
42 path,
43
44 implicitToken: true,
45 defaultExpectedStatus: HttpStatusCode.OK_200
46 })
47 }
48}
diff --git a/shared/server-commands/videos/videos-command.ts b/shared/server-commands/videos/videos-command.ts
index 21753ddc4..2ac426f76 100644
--- a/shared/server-commands/videos/videos-command.ts
+++ b/shared/server-commands/videos/videos-command.ts
@@ -107,23 +107,6 @@ export class VideosCommand extends AbstractCommand {
107 107
108 // --------------------------------------------------------------------------- 108 // ---------------------------------------------------------------------------
109 109
110 view (options: OverrideCommandOptions & {
111 id: number | string
112 xForwardedFor?: string
113 }) {
114 const { id, xForwardedFor } = options
115 const path = '/api/v1/videos/' + id + '/views'
116
117 return this.postBodyRequest({
118 ...options,
119
120 path,
121 xForwardedFor,
122 implicitToken: false,
123 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
124 })
125 }
126
127 rate (options: OverrideCommandOptions & { 110 rate (options: OverrideCommandOptions & {
128 id: number | string 111 id: number | string
129 rating: UserVideoRateType 112 rating: UserVideoRateType
diff --git a/shared/server-commands/videos/views-command.ts b/shared/server-commands/videos/views-command.ts
new file mode 100644
index 000000000..01113f798
--- /dev/null
+++ b/shared/server-commands/videos/views-command.ts
@@ -0,0 +1,51 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
2import { HttpStatusCode, VideoViewEvent } from '@shared/models'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4
5export class ViewsCommand extends AbstractCommand {
6
7 view (options: OverrideCommandOptions & {
8 id: number | string
9 currentTime?: number
10 viewEvent?: VideoViewEvent
11 xForwardedFor?: string
12 }) {
13 const { id, xForwardedFor, viewEvent, currentTime } = options
14 const path = '/api/v1/videos/' + id + '/views'
15
16 return this.postBodyRequest({
17 ...options,
18
19 path,
20 xForwardedFor,
21 fields: {
22 currentTime: currentTime ?? 1,
23 viewEvent
24 },
25 implicitToken: false,
26 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
27 })
28 }
29
30 async simulateView (options: OverrideCommandOptions & {
31 id: number | string
32 xForwardedFor?: string
33 }) {
34 await this.view({ ...options, currentTime: 0 })
35 await this.view({ ...options, currentTime: 5 })
36 }
37
38 async simulateViewer (options: OverrideCommandOptions & {
39 id: number | string
40 currentTimes: number[]
41 xForwardedFor?: string
42 }) {
43 let viewEvent: VideoViewEvent = 'seek'
44
45 for (const currentTime of options.currentTimes) {
46 await this.view({ ...options, currentTime, viewEvent })
47
48 viewEvent = undefined
49 }
50 }
51}