From b211106695bb82f6c32e53306081b5262c3d109d Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 24 Mar 2022 13:36:47 +0100 Subject: 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 --- shared/models/activitypub/activity.ts | 8 ++-- shared/models/activitypub/context.ts | 3 +- shared/models/activitypub/objects/index.ts | 1 + .../activitypub/objects/watch-action-object.ts | 22 ++++++++++ shared/models/server/debug.model.ts | 2 +- shared/models/users/index.ts | 1 - shared/models/users/user-watching-video.model.ts | 3 -- shared/models/videos/index.ts | 2 + shared/models/videos/stats/index.ts | 4 ++ .../videos/stats/video-stats-overall.model.ts | 17 ++++++++ .../videos/stats/video-stats-retention.model.ts | 6 +++ .../stats/video-stats-timeserie-metric.type.ts | 1 + .../videos/stats/video-stats-timeserie.model.ts | 6 +++ shared/models/videos/video-view.model.ts | 6 +++ shared/models/videos/video.model.ts | 3 +- shared/server-commands/server/server.ts | 8 +++- shared/server-commands/videos/history-command.ts | 19 -------- shared/server-commands/videos/index.ts | 1 + .../server-commands/videos/video-stats-command.ts | 48 ++++++++++++++++++++ shared/server-commands/videos/videos-command.ts | 17 -------- shared/server-commands/videos/views-command.ts | 51 ++++++++++++++++++++++ 21 files changed, 181 insertions(+), 48 deletions(-) create mode 100644 shared/models/activitypub/objects/watch-action-object.ts delete mode 100644 shared/models/users/user-watching-video.model.ts create mode 100644 shared/models/videos/stats/index.ts create mode 100644 shared/models/videos/stats/video-stats-overall.model.ts create mode 100644 shared/models/videos/stats/video-stats-retention.model.ts create mode 100644 shared/models/videos/stats/video-stats-timeserie-metric.type.ts create mode 100644 shared/models/videos/stats/video-stats-timeserie.model.ts create mode 100644 shared/models/videos/video-view.model.ts create mode 100644 shared/server-commands/videos/video-stats-command.ts create mode 100644 shared/server-commands/videos/views-command.ts (limited to 'shared') 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 @@ import { ActivityPubActor } from './activitypub-actor' import { ActivityPubSignature } from './activitypub-signature' -import { ActivityFlagReasonObject, CacheFileObject, VideoObject } from './objects' +import { ActivityFlagReasonObject, CacheFileObject, VideoObject, WatchActionObject } from './objects' import { AbuseObject } from './objects/abuse-object' import { DislikeObject } from './objects/dislike-object' import { APObject } from './objects/object.model' @@ -52,7 +52,7 @@ export interface BaseActivity { export interface ActivityCreate extends BaseActivity { type: 'Create' - object: VideoObject | AbuseObject | DislikeObject | VideoCommentObject | CacheFileObject | PlaylistObject + object: VideoObject | AbuseObject | DislikeObject | VideoCommentObject | CacheFileObject | PlaylistObject | WatchActionObject } export interface ActivityUpdate extends BaseActivity { @@ -99,7 +99,9 @@ export interface ActivityView extends BaseActivity { type: 'View' actor: string object: APObject - expires: string + + // If sending a "viewer" event + expires?: string } 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 = 'Rate' | 'Flag' | 'Actor' | - 'Collection' + 'Collection' | + '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' export * from './video-comment-object' export * from './video-torrent-object' export * from './view-object' +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 @@ +export interface WatchActionObject { + id: string + type: 'WatchAction' + + startTime: string + endTime: string + + location?: { + addressCountry: string + } + + uuid: string + object: string + actionStatus: 'CompletedActionStatus' + + duration: string + + watchSections: { + startTimestamp: number + endTimestamp: number + }[] +} 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 { } export interface SendDebugCommand { - command: 'remove-dandling-resumable-uploads' + command: 'remove-dandling-resumable-uploads' | 'process-video-views-buffer' | 'process-video-viewers' } 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' export * from './user-update-me.model' export * from './user-update.model' export * from './user-video-quota.model' -export * from './user-watching-video.model' 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 @@ -export interface UserWatchingVideo { - currentTime: number -} 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' export * from './import' export * from './playlist' export * from './rate' +export * from './stats' export * from './transcoding' export * from './nsfw-policy.type' @@ -32,5 +33,6 @@ export * from './video-streaming-playlist.model' export * from './video-streaming-playlist.type' export * from './video-update.model' +export * from './video-view.model' export * from './video.model' 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 @@ +export * from './video-stats-overall.model' +export * from './video-stats-retention.model' +export * from './video-stats-timeserie.model' +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 @@ +export interface VideoStatsOverall { + averageWatchTime: number + totalWatchTime: number + + viewersPeak: number + viewersPeakDate: string + + views: number + likes: number + dislikes: number + comments: number + + countries: { + isoCode: string + viewers: number + }[] +} 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 @@ +export interface VideoStatsRetention { + data: { + second: number + retentionPercent: number + }[] +} 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 @@ +export interface VideoStatsTimeserie { + data: { + date: string + value: number + }[] +} 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 @@ +export type VideoViewEvent = 'seek' + +export interface VideoView { + currentTime: number + viewEvent?: VideoViewEvent +} 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 { url: string views: number - // If live - viewers?: number + viewers: number likes: number 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 { PlaylistsCommand, ServicesCommand, StreamingPlaylistsCommand, + VideosCommand, VideoStudioCommand, - VideosCommand + ViewsCommand } from '../videos' import { CommentsCommand } from '../videos/comments-command' +import { VideoStatsCommand } from '../videos/video-stats-command' import { ConfigCommand } from './config-command' import { ContactFormCommand } from './contact-form-command' import { DebugCommand } from './debug-command' @@ -127,6 +129,8 @@ export class PeerTubeServer { objectStorage?: ObjectStorageCommand videoStudio?: VideoStudioCommand videos?: VideosCommand + videoStats?: VideoStatsCommand + views?: ViewsCommand constructor (options: { serverNumber: number } | { url: string }) { if ((options as any).url) { @@ -397,5 +401,7 @@ export class PeerTubeServer { this.videos = new VideosCommand(this) this.objectStorage = new ObjectStorageCommand(this) this.videoStudio = new VideoStudioCommand(this) + this.videoStats = new VideoStatsCommand(this) + this.views = new ViewsCommand(this) } } 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' export class HistoryCommand extends AbstractCommand { - watchVideo (options: OverrideCommandOptions & { - videoId: number | string - currentTime: number - }) { - const { videoId, currentTime } = options - - const path = '/api/v1/videos/' + videoId + '/watching' - const fields = { currentTime } - - return this.putBodyRequest({ - ...options, - - path, - fields, - implicitToken: true, - defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 - }) - } - list (options: OverrideCommandOptions & { search?: string } = {}) { 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' export * from './streaming-playlists-command' export * from './comments-command' export * from './video-studio-command' +export * from './views-command' 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 @@ +import { HttpStatusCode, VideoStatsOverall, VideoStatsRetention, VideoStatsTimeserie, VideoStatsTimeserieMetric } from '@shared/models' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +export class VideoStatsCommand extends AbstractCommand { + + getOverallStats (options: OverrideCommandOptions & { + videoId: number | string + }) { + const path = '/api/v1/videos/' + options.videoId + '/stats/overall' + + return this.getRequestBody({ + ...options, + path, + + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } + + getTimeserieStats (options: OverrideCommandOptions & { + videoId: number | string + metric: VideoStatsTimeserieMetric + }) { + const path = '/api/v1/videos/' + options.videoId + '/stats/timeseries/' + options.metric + + return this.getRequestBody({ + ...options, + path, + + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } + + getRetentionStats (options: OverrideCommandOptions & { + videoId: number | string + }) { + const path = '/api/v1/videos/' + options.videoId + '/stats/retention' + + return this.getRequestBody({ + ...options, + path, + + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } +} 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 { // --------------------------------------------------------------------------- - view (options: OverrideCommandOptions & { - id: number | string - xForwardedFor?: string - }) { - const { id, xForwardedFor } = options - const path = '/api/v1/videos/' + id + '/views' - - return this.postBodyRequest({ - ...options, - - path, - xForwardedFor, - implicitToken: false, - defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 - }) - } - rate (options: OverrideCommandOptions & { id: number | string 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 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */ +import { HttpStatusCode, VideoViewEvent } from '@shared/models' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +export class ViewsCommand extends AbstractCommand { + + view (options: OverrideCommandOptions & { + id: number | string + currentTime?: number + viewEvent?: VideoViewEvent + xForwardedFor?: string + }) { + const { id, xForwardedFor, viewEvent, currentTime } = options + const path = '/api/v1/videos/' + id + '/views' + + return this.postBodyRequest({ + ...options, + + path, + xForwardedFor, + fields: { + currentTime: currentTime ?? 1, + viewEvent + }, + implicitToken: false, + defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + } + + async simulateView (options: OverrideCommandOptions & { + id: number | string + xForwardedFor?: string + }) { + await this.view({ ...options, currentTime: 0 }) + await this.view({ ...options, currentTime: 5 }) + } + + async simulateViewer (options: OverrideCommandOptions & { + id: number | string + currentTimes: number[] + xForwardedFor?: string + }) { + let viewEvent: VideoViewEvent = 'seek' + + for (const currentTime of options.currentTimes) { + await this.view({ ...options, currentTime, viewEvent }) + + viewEvent = undefined + } + } +} -- cgit v1.2.3