diff options
author | Chocobozzz <me@florianbigard.com> | 2022-03-24 13:36:47 +0100 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2022-04-15 09:49:35 +0200 |
commit | b211106695bb82f6c32e53306081b5262c3d109d (patch) | |
tree | fa187de1c33b0956665f5362e29af6b0f6d8bb57 /shared | |
parent | 69d48ee30c9d47cddf0c3c047dc99a99dcb6e894 (diff) | |
download | PeerTube-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')
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 @@ | |||
1 | import { ActivityPubActor } from './activitypub-actor' | 1 | import { ActivityPubActor } from './activitypub-actor' |
2 | import { ActivityPubSignature } from './activitypub-signature' | 2 | import { ActivityPubSignature } from './activitypub-signature' |
3 | import { ActivityFlagReasonObject, CacheFileObject, VideoObject } from './objects' | 3 | import { ActivityFlagReasonObject, CacheFileObject, VideoObject, WatchActionObject } from './objects' |
4 | import { AbuseObject } from './objects/abuse-object' | 4 | import { AbuseObject } from './objects/abuse-object' |
5 | import { DislikeObject } from './objects/dislike-object' | 5 | import { DislikeObject } from './objects/dislike-object' |
6 | import { APObject } from './objects/object.model' | 6 | import { APObject } from './objects/object.model' |
@@ -52,7 +52,7 @@ export interface BaseActivity { | |||
52 | 52 | ||
53 | export interface ActivityCreate extends BaseActivity { | 53 | export 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 | ||
58 | export interface ActivityUpdate extends BaseActivity { | 58 | export 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 | ||
105 | export interface ActivityDislike extends BaseActivity { | 107 | export 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' | |||
8 | export * from './video-comment-object' | 8 | export * from './video-comment-object' |
9 | export * from './video-torrent-object' | 9 | export * from './video-torrent-object' |
10 | export * from './view-object' | 10 | export * from './view-object' |
11 | export * 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 @@ | |||
1 | export 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 | ||
6 | export interface SendDebugCommand { | 6 | export 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' | |||
12 | export * from './user-update-me.model' | 12 | export * from './user-update-me.model' |
13 | export * from './user-update.model' | 13 | export * from './user-update.model' |
14 | export * from './user-video-quota.model' | 14 | export * from './user-video-quota.model' |
15 | export * from './user-watching-video.model' | ||
16 | export * from './user.model' | 15 | export * 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 @@ | |||
1 | export 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' | |||
9 | export * from './import' | 9 | export * from './import' |
10 | export * from './playlist' | 10 | export * from './playlist' |
11 | export * from './rate' | 11 | export * from './rate' |
12 | export * from './stats' | ||
12 | export * from './transcoding' | 13 | export * from './transcoding' |
13 | 14 | ||
14 | export * from './nsfw-policy.type' | 15 | export * from './nsfw-policy.type' |
@@ -32,5 +33,6 @@ export * from './video-streaming-playlist.model' | |||
32 | export * from './video-streaming-playlist.type' | 33 | export * from './video-streaming-playlist.type' |
33 | 34 | ||
34 | export * from './video-update.model' | 35 | export * from './video-update.model' |
36 | export * from './video-view.model' | ||
35 | export * from './video.model' | 37 | export * from './video.model' |
36 | export * from './video-create-result.model' | 38 | export * 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 @@ | |||
1 | export * from './video-stats-overall.model' | ||
2 | export * from './video-stats-retention.model' | ||
3 | export * from './video-stats-timeserie.model' | ||
4 | export * 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 @@ | |||
1 | export 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 @@ | |||
1 | export 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 @@ | |||
1 | export 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 @@ | |||
1 | export type VideoViewEvent = 'seek' | ||
2 | |||
3 | export 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' |
31 | import { CommentsCommand } from '../videos/comments-command' | 32 | import { CommentsCommand } from '../videos/comments-command' |
33 | import { VideoStatsCommand } from '../videos/video-stats-command' | ||
32 | import { ConfigCommand } from './config-command' | 34 | import { ConfigCommand } from './config-command' |
33 | import { ContactFormCommand } from './contact-form-command' | 35 | import { ContactFormCommand } from './contact-form-command' |
34 | import { DebugCommand } from './debug-command' | 36 | import { 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 | ||
4 | export class HistoryCommand extends AbstractCommand { | 4 | export 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' | |||
13 | export * from './streaming-playlists-command' | 13 | export * from './streaming-playlists-command' |
14 | export * from './comments-command' | 14 | export * from './comments-command' |
15 | export * from './video-studio-command' | 15 | export * from './video-studio-command' |
16 | export * from './views-command' | ||
16 | export * from './videos-command' | 17 | export * 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 @@ | |||
1 | import { HttpStatusCode, VideoStatsOverall, VideoStatsRetention, VideoStatsTimeserie, VideoStatsTimeserieMetric } from '@shared/models' | ||
2 | import { AbstractCommand, OverrideCommandOptions } from '../shared' | ||
3 | |||
4 | export 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 */ | ||
2 | import { HttpStatusCode, VideoViewEvent } from '@shared/models' | ||
3 | import { AbstractCommand, OverrideCommandOptions } from '../shared' | ||
4 | |||
5 | export 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 | } | ||