aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--server/lib/stat-manager.ts27
-rw-r--r--server/models/video/video-channel.ts43
-rw-r--r--server/models/video/video-playlist.ts60
-rw-r--r--server/tests/api/server/stats.ts76
-rw-r--r--shared/models/server/server-stats.model.ts7
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'
3import { ActorFollowModel } from '@server/models/activitypub/actor-follow' 3import { ActorFollowModel } from '@server/models/activitypub/actor-follow'
4import { VideoRedundancyModel } from '@server/models/redundancy/video-redundancy' 4import { VideoRedundancyModel } from '@server/models/redundancy/video-redundancy'
5import { VideoModel } from '@server/models/video/video' 5import { VideoModel } from '@server/models/video/video'
6import { VideoChannelModel } from '@server/models/video/video-channel'
6import { VideoCommentModel } from '@server/models/video/video-comment' 7import { VideoCommentModel } from '@server/models/video/video-comment'
7import { VideoFileModel } from '@server/models/video/video-file' 8import { VideoFileModel } from '@server/models/video/video-file'
9import { VideoPlaylistModel } from '@server/models/video/video-playlist'
8import { ActivityType, ServerStats, VideoRedundancyStrategyWithManual } from '@shared/models' 10import { ActivityType, ServerStats, VideoRedundancyStrategyWithManual } from '@shared/models'
9 11
10class StatsManager { 12class 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 @@
1import { FindOptions, Includeable, literal, Op, ScopeOptions } from 'sequelize' 1import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions } from 'sequelize'
2import { 2import {
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 = `
350SELECT COUNT(DISTINCT("VideoChannelModel"."id")) AS "count"
351FROM "videoChannel" AS "VideoChannelModel"
352INNER JOIN "video" AS "Videos"
353ON "VideoChannelModel"."id" = "Videos"."channelId"
354AND ("Videos"."publishedAt" > Now() - interval '${days}d')
355INNER JOIN "account" AS "Account"
356ON "VideoChannelModel"."accountId" = "Account"."id"
357INNER JOIN "actor" AS "Account->Actor"
358ON "Account"."actorId" = "Account->Actor"."id"
359AND "Account->Actor"."serverId" IS NULL
360LEFT OUTER JOIN "server" AS "Account->Actor->Server"
361ON "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
54import { ThumbnailModel } from './thumbnail' 54import { ThumbnailModel } from './thumbnail'
55import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' 55import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel'
56import { VideoPlaylistElementModel } from './video-playlist-element' 56import { VideoPlaylistElementModel } from './video-playlist-element'
57import { ActorModel } from '../activitypub/actor'
57 58
58enum ScopeNames { 59enum 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
67type AvailableForListOptions = { 68type 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 @@
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { 5import {
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'
21import { getStats } from '../../../../shared/extra-utils/server/stats' 23import { getStats } from '../../../../shared/extra-utils/server/stats'
22import { addVideoCommentThread } from '../../../../shared/extra-utils/videos/video-comments' 24import { addVideoCommentThread } from '../../../../shared/extra-utils/videos/video-comments'
23import { ServerStats } from '../../../../shared/models/server/server-stats.model' 25import { ServerStats } from '../../../../shared/models/server/server-stats.model'
26import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
24import { ActivityType } from '@shared/models' 27import { ActivityType } from '@shared/models'
25 28
26const expect = chai.expect 29const expect = chai.expect
27 30
28describe('Test stats (excluding redundancy)', function () { 31describe('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