Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ <h4 i18n>Our platform in figures</h4>
<my-global-icon iconName="eye-open"></my-global-icon>
</div>

<div class="stat">
<strong>{{ stats().totalLocalVideoDownloads | number }}</strong>
<div i18n>downloads</div>
<my-global-icon iconName="download"></my-global-icon>
</div>

<div class="stat">
<strong>{{ stats().totalLocalVideoComments | number }}</strong>
<div i18n>comments</div>
Expand Down
1 change: 1 addition & 0 deletions client/src/app/+admin/system/jobs/jobs.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export class JobsComponent implements OnInit {
'video-studio-edition',
'video-transcoding',
'video-transcription',
'videos-downloads-stats',
'videos-views-stats'
]
jobTypeItems: SelectOptionsItem[] = this.jobTypes.map(i => ({ id: i, label: i }))
Expand Down
2 changes: 2 additions & 0 deletions client/src/app/+my-library/my-videos/my-videos.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@
<td>
<a class="pt" [routerLink]="[ '/videos/manage', video.shortUUID, 'stats' ]" i18n-title title="Navigate to the video stats page">
<ng-container i18n>{video.views, plural, =0 {No views} =1 {1 view} other {{{ video.views | myNumberFormatter }} views}}</ng-container>
<br />
<ng-container i18n>{video.downloads, plural, =0 {No download} =1 {1 download} other {{{ video.downloads | myNumberFormatter }} download}}</ng-container>
@if (video.isLive) {
<br />
<ng-container i18n>{video.viewers, plural, =0 {No viewers} =1 {1 viewer} other {{{ video.views | myNumberFormatter }} viewers}}</ng-container>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ type UpdateFromAPIOptions = {
| 'likes'
| 'aspectRatio'
| 'views'
| 'downloads'
| 'blacklisted'
| 'blacklistedReason'
| 'thumbnails'
Expand Down Expand Up @@ -173,6 +174,7 @@ export class VideoEdit {
state: VideoStateType
isLive: boolean
views: number
downloads: number
aspectRatio: number
duration: number
likes: number
Expand All @@ -197,6 +199,7 @@ export class VideoEdit {
aspectRatio: number
duration: number
views: number
downloads: number
likes: number

blacklisted: boolean
Expand Down Expand Up @@ -310,6 +313,7 @@ export class VideoEdit {
this.common.pluginData = {}

this.metadata.views = 0
this.metadata.downloads = 0
this.metadata.likes = 0

this.metadata.ownerAccountDisplayName = options.user.account.displayName
Expand Down Expand Up @@ -429,6 +433,7 @@ export class VideoEdit {
this.metadata.state = video.state.id
this.metadata.duration = video.duration
this.metadata.views = video.views
this.metadata.downloads = video.downloads
this.metadata.likes = video.likes
this.metadata.aspectRatio = video.aspectRatio
this.metadata.blacklisted = video.blacklisted
Expand Down Expand Up @@ -1115,6 +1120,7 @@ export class VideoEdit {
isLive: this.metadata.isLive,
aspectRatio: this.metadata.aspectRatio,
views: this.metadata.views,
downloads: this.metadata.downloads,
likes: this.metadata.likes,
duration: this.metadata.duration,
blacklisted: this.metadata.blacklisted,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { secondsToTime } from '@peertube/peertube-core-utils'
import {
HttpStatusCode,
LiveVideoSession,
VideoDownloadStatsTimeserieMetric,
VideoStatsOverall,
VideoStatsRetention,
VideoStatsTimeserie,
Expand All @@ -32,7 +33,7 @@ import { VideoStatsService } from './video-stats.service'

const BAR_GRAPHS = [ 'countries', 'regions', 'clients', 'devices', 'operatingSystems' ] as const
type BarGraphs = typeof BAR_GRAPHS[number]
type ActiveGraphId = VideoStatsTimeserieMetric | 'retention' | BarGraphs
type ActiveGraphId = VideoStatsTimeserieMetric | VideoDownloadStatsTimeserieMetric | 'retention' | BarGraphs

type GeoData = { name: string, viewers: number }[]

Expand Down Expand Up @@ -131,6 +132,11 @@ export class VideoStatsComponent implements OnInit {
label: $localize`Watch time`,
zoomEnabled: true
},
{
id: 'downloads',
label: $localize`Downloads`,
zoomEnabled: true
},
{
id: 'countries',
label: $localize`Countries`,
Expand Down Expand Up @@ -242,7 +248,7 @@ export class VideoStatsComponent implements OnInit {
}

private isTimeserieGraph (graphId: ActiveGraphId) {
return graphId === 'aggregateWatchTime' || graphId === 'viewers'
return graphId === 'aggregateWatchTime' || graphId === 'viewers' || graphId === 'downloads'
}

private loadOverallStats () {
Expand Down Expand Up @@ -334,6 +340,10 @@ export class VideoStatsComponent implements OnInit {
value: this.numberFormatter.transform(this.videoEdit.getVideoAttributes().views),
help: $localize`A view means that someone watched the video for several seconds (10 seconds by default)`
},
{
label: $localize`Downloads`,
value: this.numberFormatter.transform(this.videoEdit.getVideoAttributes().downloads),
},
{
label: $localize`Likes`,
value: this.numberFormatter.transform(this.videoEdit.getVideoAttributes().likes)
Expand Down Expand Up @@ -399,6 +409,12 @@ export class VideoStatsComponent implements OnInit {
endDate: this.statsEndDate,
metric: 'viewers'
}),
downloads: this.statsService.getTimeserieStats({
videoId,
startDate: this.statsStartDate,
endDate: this.statsEndDate,
metric: 'downloads'
}),

countries: of(this.countries),

Expand Down Expand Up @@ -426,6 +442,7 @@ export class VideoStatsComponent implements OnInit {
retention: (rawData: VideoStatsRetention) => this.buildRetentionChartOptions(rawData),
aggregateWatchTime: (rawData: VideoStatsTimeserie) => this.buildTimeserieChartOptions(rawData),
viewers: (rawData: VideoStatsTimeserie) => this.buildTimeserieChartOptions(rawData),
downloads: (rawData: VideoStatsTimeserie) => this.buildTimeserieChartOptions(rawData),
countries: (rawData: GeoData) => this.buildGeoChartOptions(rawData),
regions: (rawData: GeoData) => this.buildGeoChartOptions(rawData)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { HttpClient, HttpParams } from '@angular/common/http'
import { Injectable, inject } from '@angular/core'
import { RestExtractor } from '@app/core'
import {
VideoDownloadStatsTimeserieMetric,
VideoStatsOverall,
VideoStatsRetention,
VideoStatsTimeserie,
Expand Down Expand Up @@ -38,7 +39,7 @@ export class VideoStatsService {

getTimeserieStats (options: {
videoId: string
metric: VideoStatsTimeserieMetric
metric: VideoStatsTimeserieMetric | VideoDownloadStatsTimeserieMetric
startDate?: Date
endDate?: Date
}) {
Expand Down
3 changes: 3 additions & 0 deletions client/src/app/shared/shared-main/video/video.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export class Video implements VideoServerModel {
views: number
viewers: number

downloads: number

likes: number
dislikes: number

Expand Down Expand Up @@ -178,6 +180,7 @@ export class Video implements VideoServerModel {
this.viewers = hash.viewers
this.likes = hash.likes
this.dislikes = hash.dislikes
this.downloads = hash.downloads

this.nsfw = hash.nsfw
this.nsfwFlags = hash.nsfwFlags
Expand Down
8 changes: 8 additions & 0 deletions packages/models/src/activitypub/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export type Activity =
| ActivityFlag
| ActivityApproveReply
| ActivityRejectReply
| ActivityDownload

export type ActivityType =
| 'Create'
Expand All @@ -60,6 +61,7 @@ export type ActivityType =
| 'Flag'
| 'ApproveReply'
| 'RejectReply'
| 'Download'

export interface ActivityAudience {
to: string[]
Expand Down Expand Up @@ -162,3 +164,9 @@ export interface ActivityFlag extends BaseActivity {
startAt?: number
endAt?: number
}

export interface ActivityDownload extends BaseActivity {
type: 'Download'
actor: string
object: APObjectId
}
1 change: 1 addition & 0 deletions packages/models/src/activitypub/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ export type ContextType =
| 'ApproveReply'
| 'RejectReply'
| 'PlayerSettings'
| 'Download'
1 change: 1 addition & 0 deletions packages/models/src/server/debug.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface Debug {
export type SendDebugCommand = {
command:
| 'remove-dandling-resumable-uploads'
| 'process-video-downloads'
| 'process-video-views-buffer'
| 'process-video-viewers'
| 'process-video-channel-sync-latest'
Expand Down
1 change: 1 addition & 0 deletions packages/models/src/server/job.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export type JobType =
| 'video-redundancy'
| 'video-studio-edition'
| 'video-transcoding'
| 'videos-downloads-stats'
| 'videos-views-stats'
| 'generate-video-storyboard'
| 'create-user-export'
Expand Down
1 change: 1 addition & 0 deletions packages/models/src/server/server-stats.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface ServerStats extends ActivityPubMessagesSuccess, ActivityPubMess

totalLocalVideos: number
totalLocalVideoViews: number
totalLocalVideoDownloads: number
totalLocalVideoComments: number
totalLocalVideoFilesSize: number

Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export type VideoStatsTimeserieMetric = 'viewers' | 'aggregateWatchTime'
export type VideoDownloadStatsTimeserieMetric = 'downloads'
2 changes: 2 additions & 0 deletions packages/models/src/videos/video.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ export interface Video extends Partial<VideoAdditionalAttributes> {
views: number
viewers: number

downloads: number

likes: number
dislikes: number
comments: number
Expand Down
2 changes: 1 addition & 1 deletion packages/server-commands/src/server/jobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ async function waitJobs (
const states: JobState[] = [ 'waiting', 'active' ]
if (!skipDelayed) states.push('delayed')

const repeatableJobs: JobType[] = [ 'videos-views-stats', 'activitypub-cleaner' ]
const repeatableJobs: JobType[] = [ 'videos-downloads-stats', 'videos-views-stats', 'activitypub-cleaner' ]
let pendingRequests: boolean

function tasksBuilder () {
Expand Down
3 changes: 2 additions & 1 deletion packages/server-commands/src/videos/video-stats-command.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { pick } from '@peertube/peertube-core-utils'
import {
HttpStatusCode,
VideoDownloadStatsTimeserieMetric,
VideoStatsOverall,
VideoStatsRetention,
VideoStatsTimeserie,
Expand Down Expand Up @@ -53,7 +54,7 @@ export class VideoStatsCommand extends AbstractCommand {
getTimeserieStats (
options: OverrideCommandOptions & {
videoId: number | string
metric: VideoStatsTimeserieMetric
metric: VideoStatsTimeserieMetric | VideoDownloadStatsTimeserieMetric
startDate?: Date
endDate?: Date
}
Expand Down
1 change: 1 addition & 0 deletions packages/tests/src/api/downloads/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./video-downloads-counter.js"
69 changes: 69 additions & 0 deletions packages/tests/src/api/downloads/video-downloads-counter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */

import { PeerTubeServer, waitJobs } from "@peertube/peertube-server-commands"
import {
prepareDownloadsServer,
processDownloadsStats,
} from "@tests/shared/downloads.js"
import { Promise } from "bluebird"
import { expect } from "chai"

describe("Test video downloads counters", function() {
let server: PeerTubeServer

before(async function() {
this.timeout(120000)

server = await prepareDownloadsServer()

this.timeout(120000)
})

async function upload(): Promise < string > {
return new Promise(async (resolve) => {
const { uuid } = await server.videos.quickUpload({ name: "video" })
await waitJobs(server)
resolve(uuid)
})
}

it("Should count downloads", async function() {
const videoId = await upload()
const video = await server.videos.getWithToken({ id: videoId })
const videoFileIds = [ video.files[0].id ]

await server.videos.generateDownload({
videoId,
videoFileIds,
})
await processDownloadsStats([ server ])

expect((await server.videos.get({ id: videoId })).downloads).to.equal(1)
})

it("Should return time-series for downloads stats", async function() {
const videoId = await upload()
const video = await server.videos.getWithToken({ id: videoId })
const videoFileIds = [ video.files[0].id ]

await server.videos.generateDownload({
videoId,
videoFileIds,
})
await processDownloadsStats([ server ])

const startDate = new Date()
startDate.setSeconds(0)
startDate.setMilliseconds(0)

const res = await server.videoStats.getTimeserieStats({
videoId,
metric: "downloads",
})
const count = res.data.find(
(e) => e.date === startDate.toISOString(),
).value

expect(count).to.equal(1)
})
})
29 changes: 29 additions & 0 deletions packages/tests/src/shared/downloads.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {
createSingleServer,
PeerTubeServer,
setAccessTokensToServers,
setDefaultVideoChannel,
waitJobs,
} from "@peertube/peertube-server-commands"

async function prepareDownloadsServer() {
const server = await createSingleServer(1, {})
await setAccessTokensToServers([ server ])
await setDefaultVideoChannel([ server ])

await server.config.enableMinimumTranscoding()

return server
}

async function processDownloadsStats(servers: PeerTubeServer[]) {
for (const server of servers) {
await server.debug.sendCommand({
body: { command: "process-video-downloads" },
})
}

await waitJobs(servers)
}

export { prepareDownloadsServer, processDownloadsStats }
4 changes: 2 additions & 2 deletions packages/tests/src/shared/sql-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ export class SQLCommand {
}

async countVideoViewsOf (uuid: string) {
const query = 'SELECT SUM("videoView"."views") AS "total" FROM "videoView" ' +
`INNER JOIN "video" ON "video"."id" = "videoView"."videoId" WHERE "video"."uuid" = :uuid`
const query = 'SELECT SUM("videoStats"."views") AS "total" FROM "videoStats" ' +
`INNER JOIN "video" ON "video"."id" = "videoStats"."videoId" WHERE "video"."uuid" = :uuid`

const [ { total } ] = await this.selectQuery<{ total: number }>(query, { uuid })
if (!total) return 0
Expand Down
Loading
Loading