diff options
author | Rigel Kent <sendmemail@rigelk.eu> | 2021-04-12 11:19:07 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-04-12 11:19:07 +0200 |
commit | fe19f600dab0f6b00a7aa146ba4bd4bb96536155 (patch) | |
tree | be388f89a41cbc257fc9a642a9205b4910b7a6b7 | |
parent | a472cf033003cf96b69a80808b2dce1fe382e09b (diff) | |
download | PeerTube-fe19f600dab0f6b00a7aa146ba4bd4bb96536155.tar.gz PeerTube-fe19f600dab0f6b00a7aa146ba4bd4bb96536155.tar.zst PeerTube-fe19f600dab0f6b00a7aa146ba4bd4bb96536155.zip |
add channel and playlist stats to server stats endpoint (#3747)
* add channel and playlist stats to nodeinfo
* add tests for active video channels stats
* fix tests for active channel stats
-rw-r--r-- | server/lib/stat-manager.ts | 27 | ||||
-rw-r--r-- | server/models/video/video-channel.ts | 43 | ||||
-rw-r--r-- | server/models/video/video-playlist.ts | 60 | ||||
-rw-r--r-- | server/tests/api/server/stats.ts | 76 | ||||
-rw-r--r-- | shared/models/server/server-stats.model.ts | 7 |
5 files changed, 191 insertions, 22 deletions
diff --git a/server/lib/stat-manager.ts b/server/lib/stat-manager.ts index 547d7a56b..09ba208bd 100644 --- a/server/lib/stat-manager.ts +++ b/server/lib/stat-manager.ts | |||
@@ -3,8 +3,10 @@ import { UserModel } from '@server/models/account/user' | |||
3 | import { ActorFollowModel } from '@server/models/activitypub/actor-follow' | 3 | import { ActorFollowModel } from '@server/models/activitypub/actor-follow' |
4 | import { VideoRedundancyModel } from '@server/models/redundancy/video-redundancy' | 4 | import { VideoRedundancyModel } from '@server/models/redundancy/video-redundancy' |
5 | import { VideoModel } from '@server/models/video/video' | 5 | import { VideoModel } from '@server/models/video/video' |
6 | import { VideoChannelModel } from '@server/models/video/video-channel' | ||
6 | import { VideoCommentModel } from '@server/models/video/video-comment' | 7 | import { VideoCommentModel } from '@server/models/video/video-comment' |
7 | import { VideoFileModel } from '@server/models/video/video-file' | 8 | import { VideoFileModel } from '@server/models/video/video-file' |
9 | import { VideoPlaylistModel } from '@server/models/video/video-playlist' | ||
8 | import { ActivityType, ServerStats, VideoRedundancyStrategyWithManual } from '@shared/models' | 10 | import { ActivityType, ServerStats, VideoRedundancyStrategyWithManual } from '@shared/models' |
9 | 11 | ||
10 | class StatsManager { | 12 | class StatsManager { |
@@ -46,21 +48,36 @@ class StatsManager { | |||
46 | const { totalUsers, totalDailyActiveUsers, totalWeeklyActiveUsers, totalMonthlyActiveUsers } = await UserModel.getStats() | 48 | const { totalUsers, totalDailyActiveUsers, totalWeeklyActiveUsers, totalMonthlyActiveUsers } = await UserModel.getStats() |
47 | const { totalInstanceFollowers, totalInstanceFollowing } = await ActorFollowModel.getStats() | 49 | const { totalInstanceFollowers, totalInstanceFollowing } = await ActorFollowModel.getStats() |
48 | const { totalLocalVideoFilesSize } = await VideoFileModel.getStats() | 50 | const { totalLocalVideoFilesSize } = await VideoFileModel.getStats() |
51 | const { | ||
52 | totalLocalVideoChannels, | ||
53 | totalLocalDailyActiveVideoChannels, | ||
54 | totalLocalWeeklyActiveVideoChannels, | ||
55 | totalLocalMonthlyActiveVideoChannels | ||
56 | } = await VideoChannelModel.getStats() | ||
57 | const { totalLocalPlaylists } = await VideoPlaylistModel.getStats() | ||
49 | 58 | ||
50 | const videosRedundancyStats = await this.buildRedundancyStats() | 59 | const videosRedundancyStats = await this.buildRedundancyStats() |
51 | 60 | ||
52 | const data: ServerStats = { | 61 | const data: ServerStats = { |
62 | totalUsers, | ||
63 | totalDailyActiveUsers, | ||
64 | totalWeeklyActiveUsers, | ||
65 | totalMonthlyActiveUsers, | ||
66 | |||
53 | totalLocalVideos, | 67 | totalLocalVideos, |
54 | totalLocalVideoViews, | 68 | totalLocalVideoViews, |
55 | totalLocalVideoFilesSize, | ||
56 | totalLocalVideoComments, | 69 | totalLocalVideoComments, |
70 | totalLocalVideoFilesSize, | ||
71 | |||
57 | totalVideos, | 72 | totalVideos, |
58 | totalVideoComments, | 73 | totalVideoComments, |
59 | 74 | ||
60 | totalUsers, | 75 | totalLocalVideoChannels, |
61 | totalDailyActiveUsers, | 76 | totalLocalDailyActiveVideoChannels, |
62 | totalWeeklyActiveUsers, | 77 | totalLocalWeeklyActiveVideoChannels, |
63 | totalMonthlyActiveUsers, | 78 | totalLocalMonthlyActiveVideoChannels, |
79 | |||
80 | totalLocalPlaylists, | ||
64 | 81 | ||
65 | totalInstanceFollowers, | 82 | totalInstanceFollowers, |
66 | totalInstanceFollowing, | 83 | totalInstanceFollowing, |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index d2a055f5b..b7ffbd3b1 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { FindOptions, Includeable, literal, Op, ScopeOptions } from 'sequelize' | 1 | import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions } from 'sequelize' |
2 | import { | 2 | import { |
3 | AllowNull, | 3 | AllowNull, |
4 | BeforeDestroy, | 4 | BeforeDestroy, |
@@ -338,6 +338,47 @@ export class VideoChannelModel extends Model { | |||
338 | return VideoChannelModel.count(query) | 338 | return VideoChannelModel.count(query) |
339 | } | 339 | } |
340 | 340 | ||
341 | static async getStats () { | ||
342 | |||
343 | function getActiveVideoChannels (days: number) { | ||
344 | const options = { | ||
345 | type: QueryTypes.SELECT as QueryTypes.SELECT, | ||
346 | raw: true | ||
347 | } | ||
348 | |||
349 | const query = ` | ||
350 | SELECT COUNT(DISTINCT("VideoChannelModel"."id")) AS "count" | ||
351 | FROM "videoChannel" AS "VideoChannelModel" | ||
352 | INNER JOIN "video" AS "Videos" | ||
353 | ON "VideoChannelModel"."id" = "Videos"."channelId" | ||
354 | AND ("Videos"."publishedAt" > Now() - interval '${days}d') | ||
355 | INNER JOIN "account" AS "Account" | ||
356 | ON "VideoChannelModel"."accountId" = "Account"."id" | ||
357 | INNER JOIN "actor" AS "Account->Actor" | ||
358 | ON "Account"."actorId" = "Account->Actor"."id" | ||
359 | AND "Account->Actor"."serverId" IS NULL | ||
360 | LEFT OUTER JOIN "server" AS "Account->Actor->Server" | ||
361 | ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | ||
362 | |||
363 | return VideoChannelModel.sequelize.query<{ count: string }>(query, options) | ||
364 | .then(r => parseInt(r[0].count, 10)) | ||
365 | } | ||
366 | |||
367 | const totalLocalVideoChannels = await VideoChannelModel.count() | ||
368 | const totalLocalDailyActiveVideoChannels = await getActiveVideoChannels(1) | ||
369 | const totalLocalWeeklyActiveVideoChannels = await getActiveVideoChannels(7) | ||
370 | const totalLocalMonthlyActiveVideoChannels = await getActiveVideoChannels(30) | ||
371 | const totalHalfYearActiveVideoChannels = await getActiveVideoChannels(180) | ||
372 | |||
373 | return { | ||
374 | totalLocalVideoChannels, | ||
375 | totalLocalDailyActiveVideoChannels, | ||
376 | totalLocalWeeklyActiveVideoChannels, | ||
377 | totalLocalMonthlyActiveVideoChannels, | ||
378 | totalHalfYearActiveVideoChannels | ||
379 | } | ||
380 | } | ||
381 | |||
341 | static listForApi (parameters: { | 382 | static listForApi (parameters: { |
342 | actorId: number | 383 | actorId: number |
343 | start: number | 384 | start: number |
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index 49a406608..efe5be36d 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts | |||
@@ -54,6 +54,7 @@ import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getPlaylistSort, isOutdat | |||
54 | import { ThumbnailModel } from './thumbnail' | 54 | import { ThumbnailModel } from './thumbnail' |
55 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' | 55 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' |
56 | import { VideoPlaylistElementModel } from './video-playlist-element' | 56 | import { VideoPlaylistElementModel } from './video-playlist-element' |
57 | import { ActorModel } from '../activitypub/actor' | ||
57 | 58 | ||
58 | enum ScopeNames { | 59 | enum ScopeNames { |
59 | AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', | 60 | AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', |
@@ -65,7 +66,7 @@ enum ScopeNames { | |||
65 | } | 66 | } |
66 | 67 | ||
67 | type AvailableForListOptions = { | 68 | type AvailableForListOptions = { |
68 | followerActorId: number | 69 | followerActorId?: number |
69 | type?: VideoPlaylistType | 70 | type?: VideoPlaylistType |
70 | accountId?: number | 71 | accountId?: number |
71 | videoChannelId?: number | 72 | videoChannelId?: number |
@@ -134,20 +135,26 @@ type AvailableForListOptions = { | |||
134 | privacy: VideoPlaylistPrivacy.PUBLIC | 135 | privacy: VideoPlaylistPrivacy.PUBLIC |
135 | }) | 136 | }) |
136 | 137 | ||
137 | // Only list local playlists OR playlists that are on an instance followed by actorId | 138 | // Only list local playlists |
138 | const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId) | 139 | const whereActorOr: WhereOptions[] = [ |
140 | { | ||
141 | serverId: null | ||
142 | } | ||
143 | ] | ||
139 | 144 | ||
140 | whereActor = { | 145 | // … OR playlists that are on an instance followed by actorId |
141 | [Op.or]: [ | 146 | if (options.followerActorId) { |
142 | { | 147 | const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId) |
143 | serverId: null | 148 | |
144 | }, | 149 | whereActorOr.push({ |
145 | { | 150 | serverId: { |
146 | serverId: { | 151 | [Op.in]: literal(inQueryInstanceFollow) |
147 | [Op.in]: literal(inQueryInstanceFollow) | ||
148 | } | ||
149 | } | 152 | } |
150 | ] | 153 | }) |
154 | } | ||
155 | |||
156 | whereActor = { | ||
157 | [Op.or]: whereActorOr | ||
151 | } | 158 | } |
152 | } | 159 | } |
153 | 160 | ||
@@ -495,6 +502,33 @@ export class VideoPlaylistModel extends Model { | |||
495 | return '/video-playlists/embed/' + this.uuid | 502 | return '/video-playlists/embed/' + this.uuid |
496 | } | 503 | } |
497 | 504 | ||
505 | static async getStats () { | ||
506 | const totalLocalPlaylists = await VideoPlaylistModel.count({ | ||
507 | include: [ | ||
508 | { | ||
509 | model: AccountModel, | ||
510 | required: true, | ||
511 | include: [ | ||
512 | { | ||
513 | model: ActorModel, | ||
514 | required: true, | ||
515 | where: { | ||
516 | serverId: null | ||
517 | } | ||
518 | } | ||
519 | ] | ||
520 | } | ||
521 | ], | ||
522 | where: { | ||
523 | privacy: VideoPlaylistPrivacy.PUBLIC | ||
524 | } | ||
525 | }) | ||
526 | |||
527 | return { | ||
528 | totalLocalPlaylists | ||
529 | } | ||
530 | } | ||
531 | |||
498 | setAsRefreshed () { | 532 | setAsRefreshed () { |
499 | this.changed('updatedAt', true) | 533 | this.changed('updatedAt', true) |
500 | 534 | ||
diff --git a/server/tests/api/server/stats.ts b/server/tests/api/server/stats.ts index eb474c1f5..304181a6d 100644 --- a/server/tests/api/server/stats.ts +++ b/server/tests/api/server/stats.ts | |||
@@ -3,8 +3,10 @@ | |||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { | 5 | import { |
6 | addVideoChannel, | ||
6 | cleanupTests, | 7 | cleanupTests, |
7 | createUser, | 8 | createUser, |
9 | createVideoPlaylist, | ||
8 | doubleFollow, | 10 | doubleFollow, |
9 | flushAndRunMultipleServers, | 11 | flushAndRunMultipleServers, |
10 | follow, | 12 | follow, |
@@ -21,12 +23,14 @@ import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | |||
21 | import { getStats } from '../../../../shared/extra-utils/server/stats' | 23 | import { getStats } from '../../../../shared/extra-utils/server/stats' |
22 | import { addVideoCommentThread } from '../../../../shared/extra-utils/videos/video-comments' | 24 | import { addVideoCommentThread } from '../../../../shared/extra-utils/videos/video-comments' |
23 | import { ServerStats } from '../../../../shared/models/server/server-stats.model' | 25 | import { ServerStats } from '../../../../shared/models/server/server-stats.model' |
26 | import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' | ||
24 | import { ActivityType } from '@shared/models' | 27 | import { ActivityType } from '@shared/models' |
25 | 28 | ||
26 | const expect = chai.expect | 29 | const expect = chai.expect |
27 | 30 | ||
28 | describe('Test stats (excluding redundancy)', function () { | 31 | describe('Test stats (excluding redundancy)', function () { |
29 | let servers: ServerInfo[] = [] | 32 | let servers: ServerInfo[] = [] |
33 | let channelId | ||
30 | const user = { | 34 | const user = { |
31 | username: 'user1', | 35 | username: 'user1', |
32 | password: 'super_password' | 36 | password: 'super_password' |
@@ -70,6 +74,7 @@ describe('Test stats (excluding redundancy)', function () { | |||
70 | expect(data.totalVideos).to.equal(1) | 74 | expect(data.totalVideos).to.equal(1) |
71 | expect(data.totalInstanceFollowers).to.equal(2) | 75 | expect(data.totalInstanceFollowers).to.equal(2) |
72 | expect(data.totalInstanceFollowing).to.equal(1) | 76 | expect(data.totalInstanceFollowing).to.equal(1) |
77 | expect(data.totalLocalPlaylists).to.equal(0) | ||
73 | }) | 78 | }) |
74 | 79 | ||
75 | it('Should have the correct stats on instance 2', async function () { | 80 | it('Should have the correct stats on instance 2', async function () { |
@@ -85,6 +90,7 @@ describe('Test stats (excluding redundancy)', function () { | |||
85 | expect(data.totalVideos).to.equal(1) | 90 | expect(data.totalVideos).to.equal(1) |
86 | expect(data.totalInstanceFollowers).to.equal(1) | 91 | expect(data.totalInstanceFollowers).to.equal(1) |
87 | expect(data.totalInstanceFollowing).to.equal(1) | 92 | expect(data.totalInstanceFollowing).to.equal(1) |
93 | expect(data.totalLocalPlaylists).to.equal(0) | ||
88 | }) | 94 | }) |
89 | 95 | ||
90 | it('Should have the correct stats on instance 3', async function () { | 96 | it('Should have the correct stats on instance 3', async function () { |
@@ -99,6 +105,7 @@ describe('Test stats (excluding redundancy)', function () { | |||
99 | expect(data.totalVideos).to.equal(1) | 105 | expect(data.totalVideos).to.equal(1) |
100 | expect(data.totalInstanceFollowing).to.equal(1) | 106 | expect(data.totalInstanceFollowing).to.equal(1) |
101 | expect(data.totalInstanceFollowers).to.equal(0) | 107 | expect(data.totalInstanceFollowers).to.equal(0) |
108 | expect(data.totalLocalPlaylists).to.equal(0) | ||
102 | }) | 109 | }) |
103 | 110 | ||
104 | it('Should have the correct total videos stats after an unfollow', async function () { | 111 | it('Should have the correct total videos stats after an unfollow', async function () { |
@@ -113,7 +120,7 @@ describe('Test stats (excluding redundancy)', function () { | |||
113 | expect(data.totalVideos).to.equal(0) | 120 | expect(data.totalVideos).to.equal(0) |
114 | }) | 121 | }) |
115 | 122 | ||
116 | it('Should have the correct active users stats', async function () { | 123 | it('Should have the correct active user stats', async function () { |
117 | const server = servers[0] | 124 | const server = servers[0] |
118 | 125 | ||
119 | { | 126 | { |
@@ -135,6 +142,69 @@ describe('Test stats (excluding redundancy)', function () { | |||
135 | } | 142 | } |
136 | }) | 143 | }) |
137 | 144 | ||
145 | it('Should have the correct active channel stats', async function () { | ||
146 | const server = servers[0] | ||
147 | |||
148 | { | ||
149 | const res = await getStats(server.url) | ||
150 | const data: ServerStats = res.body | ||
151 | expect(data.totalLocalDailyActiveVideoChannels).to.equal(1) | ||
152 | expect(data.totalLocalWeeklyActiveVideoChannels).to.equal(1) | ||
153 | expect(data.totalLocalMonthlyActiveVideoChannels).to.equal(1) | ||
154 | } | ||
155 | |||
156 | { | ||
157 | const channelAttributes = { | ||
158 | name: 'stats_channel', | ||
159 | displayName: 'My stats channel' | ||
160 | } | ||
161 | const resChannel = await addVideoChannel(server.url, server.accessToken, channelAttributes) | ||
162 | channelId = resChannel.body.videoChannel.id | ||
163 | |||
164 | const res = await getStats(server.url) | ||
165 | const data: ServerStats = res.body | ||
166 | expect(data.totalLocalDailyActiveVideoChannels).to.equal(1) | ||
167 | expect(data.totalLocalWeeklyActiveVideoChannels).to.equal(1) | ||
168 | expect(data.totalLocalMonthlyActiveVideoChannels).to.equal(1) | ||
169 | } | ||
170 | |||
171 | { | ||
172 | await uploadVideo(server.url, server.accessToken, { fixture: 'video_short.webm', channelId }) | ||
173 | |||
174 | const res = await getStats(server.url) | ||
175 | const data: ServerStats = res.body | ||
176 | expect(data.totalLocalDailyActiveVideoChannels).to.equal(2) | ||
177 | expect(data.totalLocalWeeklyActiveVideoChannels).to.equal(2) | ||
178 | expect(data.totalLocalMonthlyActiveVideoChannels).to.equal(2) | ||
179 | } | ||
180 | }) | ||
181 | |||
182 | it('Should have the correct playlist stats', async function () { | ||
183 | const server = servers[0] | ||
184 | |||
185 | { | ||
186 | const resStats = await getStats(server.url) | ||
187 | const dataStats: ServerStats = resStats.body | ||
188 | expect(dataStats.totalLocalPlaylists).to.equal(0) | ||
189 | } | ||
190 | |||
191 | { | ||
192 | await createVideoPlaylist({ | ||
193 | url: server.url, | ||
194 | token: server.accessToken, | ||
195 | playlistAttrs: { | ||
196 | displayName: 'playlist for count', | ||
197 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
198 | videoChannelId: channelId | ||
199 | } | ||
200 | }) | ||
201 | |||
202 | const resStats = await getStats(server.url) | ||
203 | const dataStats: ServerStats = resStats.body | ||
204 | expect(dataStats.totalLocalPlaylists).to.equal(1) | ||
205 | } | ||
206 | }) | ||
207 | |||
138 | it('Should correctly count video file sizes if transcoding is enabled', async function () { | 208 | it('Should correctly count video file sizes if transcoding is enabled', async function () { |
139 | this.timeout(60000) | 209 | this.timeout(60000) |
140 | 210 | ||
@@ -173,8 +243,8 @@ describe('Test stats (excluding redundancy)', function () { | |||
173 | { | 243 | { |
174 | const res = await getStats(servers[0].url) | 244 | const res = await getStats(servers[0].url) |
175 | const data: ServerStats = res.body | 245 | const data: ServerStats = res.body |
176 | expect(data.totalLocalVideoFilesSize).to.be.greaterThan(300000) | 246 | expect(data.totalLocalVideoFilesSize).to.be.greaterThan(500000) |
177 | expect(data.totalLocalVideoFilesSize).to.be.lessThan(400000) | 247 | expect(data.totalLocalVideoFilesSize).to.be.lessThan(600000) |
178 | } | 248 | } |
179 | }) | 249 | }) |
180 | 250 | ||
diff --git a/shared/models/server/server-stats.model.ts b/shared/models/server/server-stats.model.ts index 0f8cfc6cf..b1dcf2065 100644 --- a/shared/models/server/server-stats.model.ts +++ b/shared/models/server/server-stats.model.ts | |||
@@ -13,6 +13,13 @@ export interface ServerStats { | |||
13 | totalVideos: number | 13 | totalVideos: number |
14 | totalVideoComments: number | 14 | totalVideoComments: number |
15 | 15 | ||
16 | totalLocalVideoChannels: number | ||
17 | totalLocalDailyActiveVideoChannels: number | ||
18 | totalLocalWeeklyActiveVideoChannels: number | ||
19 | totalLocalMonthlyActiveVideoChannels: number | ||
20 | |||
21 | totalLocalPlaylists: number | ||
22 | |||
16 | totalInstanceFollowers: number | 23 | totalInstanceFollowers: number |
17 | totalInstanceFollowing: number | 24 | totalInstanceFollowing: number |
18 | 25 | ||