mirror of https://github.com/Chocobozzz/PeerTube
Add stats route
parent
22b59e8099
commit
09cababd79
|
@ -13,7 +13,7 @@ import {
|
||||||
TRANSCODING_THREADS
|
TRANSCODING_THREADS
|
||||||
} from '@app/shared/forms/form-validators/custom-config'
|
} from '@app/shared/forms/form-validators/custom-config'
|
||||||
import { NotificationsService } from 'angular2-notifications'
|
import { NotificationsService } from 'angular2-notifications'
|
||||||
import { CustomConfig } from '../../../../../../shared/models/config/custom-config.model'
|
import { CustomConfig } from '../../../../../../shared/models/server/custom-config.model'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-edit-custom-config',
|
selector: 'my-edit-custom-config',
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { HttpClient } from '@angular/common/http'
|
import { HttpClient } from '@angular/common/http'
|
||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { CustomConfig } from '../../../../../../shared/models/config/custom-config.model'
|
import { CustomConfig } from '../../../../../../shared/models/server/custom-config.model'
|
||||||
import { environment } from '../../../../environments/environment'
|
import { environment } from '../../../../environments/environment'
|
||||||
import { RestExtractor, RestService } from '../../../shared'
|
import { RestExtractor, RestService } from '../../../shared'
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import 'rxjs/add/operator/map'
|
||||||
import { Observable } from 'rxjs/Observable'
|
import { Observable } from 'rxjs/Observable'
|
||||||
import { ResultList } from '../../../../../../shared'
|
import { ResultList } from '../../../../../../shared'
|
||||||
import { JobState } from '../../../../../../shared/models'
|
import { JobState } from '../../../../../../shared/models'
|
||||||
import { Job } from '../../../../../../shared/models/job.model'
|
import { Job } from '../../../../../../shared/models/server/job.model'
|
||||||
import { environment } from '../../../../environments/environment'
|
import { environment } from '../../../../environments/environment'
|
||||||
import { RestExtractor, RestPagination, RestService } from '../../../shared'
|
import { RestExtractor, RestPagination, RestService } from '../../../shared'
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Injectable } from '@angular/core'
|
||||||
import 'rxjs/add/operator/do'
|
import 'rxjs/add/operator/do'
|
||||||
import { ReplaySubject } from 'rxjs/ReplaySubject'
|
import { ReplaySubject } from 'rxjs/ReplaySubject'
|
||||||
import { ServerConfig } from '../../../../../shared'
|
import { ServerConfig } from '../../../../../shared'
|
||||||
import { About } from '../../../../../shared/models/config/about.model'
|
import { About } from '../../../../../shared/models/server/about.model'
|
||||||
import { environment } from '../../../environments/environment'
|
import { environment } from '../../../environments/environment'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|
|
@ -56,6 +56,8 @@ async function inboxController (req: express.Request, res: express.Response, nex
|
||||||
specificActor = res.locals.videoChannel
|
specificActor = res.locals.videoChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info('Receiving inbox requests for %d activities by %s.', activities.length, res.locals.signature.actor)
|
||||||
|
|
||||||
await processActivities(activities, res.locals.signature.actor, specificActor)
|
await processActivities(activities, res.locals.signature.actor, specificActor)
|
||||||
|
|
||||||
res.status(204).end()
|
res.status(204).end()
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
import { omit } from 'lodash'
|
import { omit } from 'lodash'
|
||||||
import { ServerConfig, UserRight } from '../../../shared'
|
import { ServerConfig, UserRight } from '../../../shared'
|
||||||
import { About } from '../../../shared/models/config/about.model'
|
import { About } from '../../../shared/models/server/about.model'
|
||||||
import { CustomConfig } from '../../../shared/models/config/custom-config.model'
|
import { CustomConfig } from '../../../shared/models/server/custom-config.model'
|
||||||
import { unlinkPromise, writeFilePromise } from '../../helpers/core-utils'
|
import { unlinkPromise, writeFilePromise } from '../../helpers/core-utils'
|
||||||
import { isSignupAllowed } from '../../helpers/utils'
|
import { isSignupAllowed } from '../../helpers/utils'
|
||||||
import { CONFIG, CONSTRAINTS_FIELDS, reloadConfig } from '../../initializers'
|
import { CONFIG, CONSTRAINTS_FIELDS, reloadConfig } from '../../initializers'
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
import { serverFollowsRouter } from './follows'
|
import { serverFollowsRouter } from './follows'
|
||||||
|
import { statsRouter } from './stats'
|
||||||
|
|
||||||
const serverRouter = express.Router()
|
const serverRouter = express.Router()
|
||||||
|
|
||||||
serverRouter.use('/', serverFollowsRouter)
|
serverRouter.use('/', serverFollowsRouter)
|
||||||
|
serverRouter.use('/', statsRouter)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
import * as express from 'express'
|
||||||
|
import { ServerStats } from '../../../../shared/models/server/server-stats.model'
|
||||||
|
import { asyncMiddleware } from '../../../middlewares'
|
||||||
|
import { UserModel } from '../../../models/account/user'
|
||||||
|
import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
|
||||||
|
import { VideoModel } from '../../../models/video/video'
|
||||||
|
import { VideoCommentModel } from '../../../models/video/video-comment'
|
||||||
|
|
||||||
|
const statsRouter = express.Router()
|
||||||
|
|
||||||
|
statsRouter.get('/stats',
|
||||||
|
asyncMiddleware(getStats)
|
||||||
|
)
|
||||||
|
|
||||||
|
async function getStats (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||||
|
const { totalLocalVideos, totalLocalVideoViews, totalVideos } = await VideoModel.getStats()
|
||||||
|
const { totalLocalVideoComments, totalVideoComments } = await VideoCommentModel.getStats()
|
||||||
|
const { totalUsers } = await UserModel.getStats()
|
||||||
|
const { totalInstanceFollowers, totalInstanceFollowing } = await ActorFollowModel.getStats()
|
||||||
|
|
||||||
|
const data: ServerStats = {
|
||||||
|
totalLocalVideos,
|
||||||
|
totalLocalVideoViews,
|
||||||
|
totalVideos,
|
||||||
|
totalLocalVideoComments,
|
||||||
|
totalVideoComments,
|
||||||
|
totalUsers,
|
||||||
|
totalInstanceFollowers,
|
||||||
|
totalInstanceFollowing
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json(data).end()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export {
|
||||||
|
statsRouter
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
|
||||||
import { OAuthTokenModel } from '../oauth/oauth-token'
|
import { OAuthTokenModel } from '../oauth/oauth-token'
|
||||||
import { getSort, throwIfNotValid } from '../utils'
|
import { getSort, throwIfNotValid } from '../utils'
|
||||||
import { VideoChannelModel } from '../video/video-channel'
|
import { VideoChannelModel } from '../video/video-channel'
|
||||||
|
import { VideoCommentModel } from '../video/video-comment'
|
||||||
import { AccountModel } from './account'
|
import { AccountModel } from './account'
|
||||||
|
|
||||||
@DefaultScope({
|
@DefaultScope({
|
||||||
|
@ -226,6 +227,14 @@ export class UserModel extends Model<UserModel> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async getStats () {
|
||||||
|
const totalUsers = await UserModel.count()
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalUsers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
hasRight (right: UserRight) {
|
hasRight (right: UserRight) {
|
||||||
return hasUserRight(this.role, right)
|
return hasUserRight(this.role, right)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
import { FollowState } from '../../../shared/models/actors'
|
import { FollowState } from '../../../shared/models/actors'
|
||||||
import { AccountFollow } from '../../../shared/models/actors/follow.model'
|
import { AccountFollow } from '../../../shared/models/actors/follow.model'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
|
import { getServerActor } from '../../helpers/utils'
|
||||||
import { ACTOR_FOLLOW_SCORE } from '../../initializers'
|
import { ACTOR_FOLLOW_SCORE } from '../../initializers'
|
||||||
import { FOLLOW_STATES } from '../../initializers/constants'
|
import { FOLLOW_STATES } from '../../initializers/constants'
|
||||||
import { ServerModel } from '../server/server'
|
import { ServerModel } from '../server/server'
|
||||||
|
@ -182,34 +183,6 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
|
||||||
return ActorFollowModel.findOne(query)
|
return ActorFollowModel.findOne(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
static loadByFollowerInbox (url: string, t?: Sequelize.Transaction) {
|
|
||||||
const query = {
|
|
||||||
where: {
|
|
||||||
state: 'accepted'
|
|
||||||
},
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: ActorModel,
|
|
||||||
required: true,
|
|
||||||
as: 'ActorFollower',
|
|
||||||
where: {
|
|
||||||
[Sequelize.Op.or]: [
|
|
||||||
{
|
|
||||||
inboxUrl: url
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sharedInboxUrl: url
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
transaction: t
|
|
||||||
} as any // FIXME: typings does not work
|
|
||||||
|
|
||||||
return ActorFollowModel.findOne(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
static listFollowingForApi (id: number, start: number, count: number, sort: string) {
|
static listFollowingForApi (id: number, start: number, count: number, sort: string) {
|
||||||
const query = {
|
const query = {
|
||||||
distinct: true,
|
distinct: true,
|
||||||
|
@ -296,6 +269,27 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
|
||||||
return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count)
|
return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async getStats () {
|
||||||
|
const serverActor = await getServerActor()
|
||||||
|
|
||||||
|
const totalInstanceFollowing = await ActorFollowModel.count({
|
||||||
|
where: {
|
||||||
|
actorId: serverActor.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalInstanceFollowers = await ActorFollowModel.count({
|
||||||
|
where: {
|
||||||
|
targetActorId: serverActor.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalInstanceFollowing,
|
||||||
|
totalInstanceFollowers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static async createListAcceptedFollowForApiQuery (
|
private static async createListAcceptedFollowForApiQuery (
|
||||||
type: 'followers' | 'following',
|
type: 'followers' | 'following',
|
||||||
actorIds: number[],
|
actorIds: number[],
|
||||||
|
|
|
@ -326,6 +326,32 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
|
||||||
.findAll(query)
|
.findAll(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async getStats () {
|
||||||
|
const totalLocalVideoComments = await VideoCommentModel.count({
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: AccountModel,
|
||||||
|
required: true,
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: ActorModel,
|
||||||
|
required: true,
|
||||||
|
where: {
|
||||||
|
serverId: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
const totalVideoComments = await VideoCommentModel.count()
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalLocalVideoComments,
|
||||||
|
totalVideoComments
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getThreadId (): number {
|
getThreadId (): number {
|
||||||
return this.originCommentId || this.id
|
return this.originCommentId || this.id
|
||||||
}
|
}
|
||||||
|
|
|
@ -761,6 +761,29 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
.findOne(options)
|
.findOne(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async getStats () {
|
||||||
|
const totalLocalVideos = await VideoModel.count({
|
||||||
|
where: {
|
||||||
|
remote: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const totalVideos = await VideoModel.count()
|
||||||
|
|
||||||
|
let totalLocalVideoViews = await VideoModel.sum('views', {
|
||||||
|
where: {
|
||||||
|
remote: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Sequelize could return null...
|
||||||
|
if (!totalLocalVideoViews) totalLocalVideoViews = 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalLocalVideos,
|
||||||
|
totalLocalVideoViews,
|
||||||
|
totalVideos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getOriginalFile () {
|
getOriginalFile () {
|
||||||
if (Array.isArray(this.VideoFiles) === false) return undefined
|
if (Array.isArray(this.VideoFiles) === false) return undefined
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { omit } from 'lodash'
|
import { omit } from 'lodash'
|
||||||
import 'mocha'
|
import 'mocha'
|
||||||
import { CustomConfig } from '../../../../shared/models/config/custom-config.model'
|
import { CustomConfig } from '../../../../shared/models/server/custom-config.model'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createUser, flushTests, killallServers, makeDeleteRequest, makeGetRequest, makePutBodyRequest, runServer, ServerInfo,
|
createUser, flushTests, killallServers, makeDeleteRequest, makeGetRequest, makePutBodyRequest, runServer, ServerInfo,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Order of the tests we want to execute
|
// Order of the tests we want to execute
|
||||||
import './server/config'
|
import './server/stats'
|
||||||
import './check-params'
|
import './check-params'
|
||||||
import './users/users'
|
import './users/users'
|
||||||
import './videos/single-server'
|
import './videos/single-server'
|
||||||
|
@ -10,3 +10,4 @@ import './videos/video-description'
|
||||||
import './videos/video-privacy'
|
import './videos/video-privacy'
|
||||||
import './videos/services'
|
import './videos/services'
|
||||||
import './server/email'
|
import './server/email'
|
||||||
|
import './server/config'
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
// Order of the tests we want to execute
|
// Order of the tests we want to execute
|
||||||
// import './multiple-servers'
|
|
||||||
import './videos/video-transcoder'
|
import './videos/video-transcoder'
|
||||||
import './videos/multiple-servers'
|
import './videos/multiple-servers'
|
||||||
import './server/follows'
|
import './server/follows'
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
import 'mocha'
|
import 'mocha'
|
||||||
import * as chai from 'chai'
|
import * as chai from 'chai'
|
||||||
import { About } from '../../../../shared/models/config/about.model'
|
import { About } from '../../../../shared/models/server/about.model'
|
||||||
import { CustomConfig } from '../../../../shared/models/config/custom-config.model'
|
import { CustomConfig } from '../../../../shared/models/server/custom-config.model'
|
||||||
import { deleteCustomConfig, getAbout, killallServers, reRunServer } from '../../utils'
|
import { deleteCustomConfig, getAbout, killallServers, reRunServer } from '../../utils'
|
||||||
const expect = chai.expect
|
const expect = chai.expect
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
/* tslint:disable:no-unused-expression */
|
||||||
|
|
||||||
|
import * as chai from 'chai'
|
||||||
|
import 'mocha'
|
||||||
|
import { ServerStats } from '../../../../shared/models/server/server-stats.model'
|
||||||
|
import {
|
||||||
|
createUser,
|
||||||
|
doubleFollow,
|
||||||
|
flushAndRunMultipleServers,
|
||||||
|
follow,
|
||||||
|
killallServers,
|
||||||
|
ServerInfo,
|
||||||
|
uploadVideo,
|
||||||
|
viewVideo,
|
||||||
|
wait
|
||||||
|
} from '../../utils'
|
||||||
|
import { flushTests, setAccessTokensToServers } from '../../utils/index'
|
||||||
|
import { getStats } from '../../utils/server/stats'
|
||||||
|
import { addVideoCommentThread } from '../../utils/videos/video-comments'
|
||||||
|
|
||||||
|
const expect = chai.expect
|
||||||
|
|
||||||
|
describe('Test stats', function () {
|
||||||
|
let servers: ServerInfo[] = []
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(60000)
|
||||||
|
|
||||||
|
await flushTests()
|
||||||
|
servers = await flushAndRunMultipleServers(3)
|
||||||
|
await setAccessTokensToServers(servers)
|
||||||
|
|
||||||
|
await doubleFollow(servers[0], servers[1])
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
username: 'user1',
|
||||||
|
password: 'super_password'
|
||||||
|
}
|
||||||
|
await createUser(servers[0].url, servers[0].accessToken, user.username, user.password)
|
||||||
|
|
||||||
|
const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, {})
|
||||||
|
const videoUUID = resVideo.body.video.uuid
|
||||||
|
|
||||||
|
await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoUUID, 'comment')
|
||||||
|
|
||||||
|
await viewVideo(servers[0].url, videoUUID)
|
||||||
|
|
||||||
|
await follow(servers[2].url, [ servers[0].url ], servers[2].accessToken)
|
||||||
|
await wait(5000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should have the correct stats on instance 1', async function () {
|
||||||
|
const res = await getStats(servers[0].url)
|
||||||
|
const data: ServerStats = res.body
|
||||||
|
|
||||||
|
expect(data.totalLocalVideoComments).to.equal(1)
|
||||||
|
expect(data.totalLocalVideos).to.equal(1)
|
||||||
|
expect(data.totalLocalVideoViews).to.equal(1)
|
||||||
|
expect(data.totalUsers).to.equal(2)
|
||||||
|
expect(data.totalVideoComments).to.equal(1)
|
||||||
|
expect(data.totalVideos).to.equal(1)
|
||||||
|
expect(data.totalInstanceFollowers).to.equal(2)
|
||||||
|
expect(data.totalInstanceFollowing).to.equal(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should have the correct stats on instance 2', async function () {
|
||||||
|
const res = await getStats(servers[1].url)
|
||||||
|
const data: ServerStats = res.body
|
||||||
|
|
||||||
|
expect(data.totalLocalVideoComments).to.equal(0)
|
||||||
|
expect(data.totalLocalVideos).to.equal(0)
|
||||||
|
expect(data.totalLocalVideoViews).to.equal(0)
|
||||||
|
expect(data.totalUsers).to.equal(1)
|
||||||
|
expect(data.totalVideoComments).to.equal(1)
|
||||||
|
expect(data.totalVideos).to.equal(1)
|
||||||
|
expect(data.totalInstanceFollowers).to.equal(1)
|
||||||
|
expect(data.totalInstanceFollowing).to.equal(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should have the correct stats on instance 3', async function () {
|
||||||
|
const res = await getStats(servers[2].url)
|
||||||
|
const data: ServerStats = res.body
|
||||||
|
|
||||||
|
expect(data.totalLocalVideoComments).to.equal(0)
|
||||||
|
expect(data.totalLocalVideos).to.equal(0)
|
||||||
|
expect(data.totalLocalVideoViews).to.equal(0)
|
||||||
|
expect(data.totalUsers).to.equal(1)
|
||||||
|
expect(data.totalVideoComments).to.equal(1)
|
||||||
|
expect(data.totalVideos).to.equal(1)
|
||||||
|
expect(data.totalInstanceFollowing).to.equal(1)
|
||||||
|
expect(data.totalInstanceFollowers).to.equal(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async function () {
|
||||||
|
killallServers(servers)
|
||||||
|
|
||||||
|
// Keep the logs if the test failed
|
||||||
|
if (this['ok']) {
|
||||||
|
await flushTests()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,5 +1,5 @@
|
||||||
import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../'
|
import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../'
|
||||||
import { CustomConfig } from '../../../../shared/models/config/custom-config.model'
|
import { CustomConfig } from '../../../../shared/models/server/custom-config.model'
|
||||||
|
|
||||||
function getConfig (url: string) {
|
function getConfig (url: string) {
|
||||||
const path = '/api/v1/config'
|
const path = '/api/v1/config'
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { makeGetRequest } from '../'
|
||||||
|
|
||||||
|
function getStats (url: string) {
|
||||||
|
const path = '/api/v1/server/stats'
|
||||||
|
|
||||||
|
return makeGetRequest({
|
||||||
|
url,
|
||||||
|
path,
|
||||||
|
statusCodeExpected: 200
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export {
|
||||||
|
getStats
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ export * from './actors'
|
||||||
export * from './activitypub'
|
export * from './activitypub'
|
||||||
export * from './users'
|
export * from './users'
|
||||||
export * from './videos'
|
export * from './videos'
|
||||||
export * from './job.model'
|
export * from './server/job.model'
|
||||||
export * from './oauth-client-local.model'
|
export * from './oauth-client-local.model'
|
||||||
export * from './result-list.model'
|
export * from './result-list.model'
|
||||||
export * from './config/server-config.model'
|
export * from './server/server-config.model'
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
export interface ServerStats {
|
||||||
|
totalUsers: number
|
||||||
|
totalLocalVideos: number
|
||||||
|
totalLocalVideoViews: number
|
||||||
|
totalLocalVideoComments: number
|
||||||
|
|
||||||
|
totalVideos: number
|
||||||
|
totalVideoComments: number
|
||||||
|
|
||||||
|
totalInstanceFollowers: number
|
||||||
|
totalInstanceFollowing: number
|
||||||
|
}
|
Loading…
Reference in New Issue