mirror of https://github.com/Chocobozzz/PeerTube
Use inner join and document code for viewr stats for channels
parent
714bfcc556
commit
3d527ba173
|
@ -56,6 +56,7 @@
|
||||||
"@ngx-loading-bar/router": "^4.2.0",
|
"@ngx-loading-bar/router": "^4.2.0",
|
||||||
"@ngx-meta/core": "^8.0.2",
|
"@ngx-meta/core": "^8.0.2",
|
||||||
"@ngx-translate/i18n-polyfill": "^1.0.0",
|
"@ngx-translate/i18n-polyfill": "^1.0.0",
|
||||||
|
"@types/chart.js": "^2.9.16",
|
||||||
"@types/core-js": "^2.5.2",
|
"@types/core-js": "^2.5.2",
|
||||||
"@types/debug": "^4.1.5",
|
"@types/debug": "^4.1.5",
|
||||||
"@types/hls.js": "^0.12.4",
|
"@types/hls.js": "^0.12.4",
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
<div i18n class="video-channel-followers">{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div>
|
<div i18n class="video-channel-followers">{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div>
|
||||||
|
|
||||||
<div *ngIf="!isInSmallView" class="w-100 d-flex justify-content-end">
|
<div *ngIf="!isInSmallView" class="w-100 d-flex justify-content-end">
|
||||||
<p-chart *ngIf="videoChannelsData && videoChannelsData[i]" type="line" [data]="videoChannelsData[i]" [options]="chartOptions" width="40vw" height="100px"></p-chart>
|
<p-chart *ngIf="videoChannelsChartData && videoChannelsChartData[i]" type="line" [data]="videoChannelsChartData[i]" [options]="chartOptions" width="40vw" height="100px"></p-chart>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,8 @@ import { ScreenService } from '@app/shared/misc/screen.service'
|
||||||
import { User } from '@app/shared'
|
import { User } from '@app/shared'
|
||||||
import { flatMap } from 'rxjs/operators'
|
import { flatMap } from 'rxjs/operators'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { minBy, maxBy } from 'lodash-es'
|
import { min, minBy, max, maxBy } from 'lodash-es'
|
||||||
|
import { ChartData } from 'chart.js'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-account-video-channels',
|
selector: 'my-account-video-channels',
|
||||||
|
@ -17,7 +18,7 @@ import { minBy, maxBy } from 'lodash-es'
|
||||||
})
|
})
|
||||||
export class MyAccountVideoChannelsComponent implements OnInit {
|
export class MyAccountVideoChannelsComponent implements OnInit {
|
||||||
videoChannels: VideoChannel[] = []
|
videoChannels: VideoChannel[] = []
|
||||||
videoChannelsData: any[]
|
videoChannelsChartData: ChartData[]
|
||||||
videoChannelsMinimumDailyViews = 0
|
videoChannelsMinimumDailyViews = 0
|
||||||
videoChannelsMaximumDailyViews: number
|
videoChannelsMaximumDailyViews: number
|
||||||
|
|
||||||
|
@ -125,7 +126,9 @@ export class MyAccountVideoChannelsComponent implements OnInit {
|
||||||
.pipe(flatMap(() => this.videoChannelService.listAccountVideoChannels(this.user.account, null, true)))
|
.pipe(flatMap(() => this.videoChannelService.listAccountVideoChannels(this.user.account, null, true)))
|
||||||
.subscribe(res => {
|
.subscribe(res => {
|
||||||
this.videoChannels = res.data
|
this.videoChannels = res.data
|
||||||
this.videoChannelsData = this.videoChannels.map(v => ({
|
|
||||||
|
// chart data
|
||||||
|
this.videoChannelsChartData = this.videoChannels.map(v => ({
|
||||||
labels: v.viewsPerDay.map(day => day.date.toLocaleDateString()),
|
labels: v.viewsPerDay.map(day => day.date.toLocaleDateString()),
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
|
@ -135,9 +138,22 @@ export class MyAccountVideoChannelsComponent implements OnInit {
|
||||||
borderColor: "#c6c6c6"
|
borderColor: "#c6c6c6"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}))
|
} as ChartData))
|
||||||
this.videoChannelsMinimumDailyViews = minBy(this.videoChannels.map(v => minBy(v.viewsPerDay, day => day.views)), day => day.views).views
|
|
||||||
this.videoChannelsMaximumDailyViews = maxBy(this.videoChannels.map(v => maxBy(v.viewsPerDay, day => day.views)), day => day.views).views
|
// chart options that depend on chart data:
|
||||||
|
// we don't want to skew values and have min at 0, so we define what the floor/ceiling is here
|
||||||
|
this.videoChannelsMinimumDailyViews = min(
|
||||||
|
this.videoChannels.map(v => minBy( // compute local minimum daily views for each channel, by their "views" attribute
|
||||||
|
v.viewsPerDay,
|
||||||
|
day => day.views
|
||||||
|
).views) // the object returned is a ViewPerDate, so we still need to get the views attribute
|
||||||
|
)
|
||||||
|
this.videoChannelsMaximumDailyViews = max(
|
||||||
|
this.videoChannels.map(v => maxBy( // compute local maximum daily views for each channel, by their "views" attribute
|
||||||
|
v.viewsPerDay,
|
||||||
|
day => day.views
|
||||||
|
).views) // the object returned is a ViewPerDate, so we still need to get the views attribute
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { VideoChannel as ServerVideoChannel, viewsPerTime } from '../../../../../shared/models/videos'
|
import { VideoChannel as ServerVideoChannel, ViewsPerDate } from '../../../../../shared/models/videos'
|
||||||
import { Actor } from '../actor/actor.model'
|
import { Actor } from '../actor/actor.model'
|
||||||
import { Account } from '../../../../../shared/models/actors'
|
import { Account } from '../../../../../shared/models/actors'
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
|
||||||
ownerAccount?: Account
|
ownerAccount?: Account
|
||||||
ownerBy?: string
|
ownerBy?: string
|
||||||
ownerAvatarUrl?: string
|
ownerAvatarUrl?: string
|
||||||
viewsPerDay?: viewsPerTime[]
|
viewsPerDay?: ViewsPerDate[]
|
||||||
|
|
||||||
constructor (hash: ServerVideoChannel) {
|
constructor (hash: ServerVideoChannel) {
|
||||||
super(hash)
|
super(hash)
|
||||||
|
|
|
@ -1149,6 +1149,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/chart.js@^2.9.16":
|
||||||
|
version "2.9.16"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/chart.js/-/chart.js-2.9.16.tgz#ac9d268fa192c0ec0efd740f802683e3ed97642c"
|
||||||
|
integrity sha512-Mofg7xFIeAWME46YMVKHPCyUz2Z0KsVMNE1f4oF3T74mK3RiPQxOm9qzoeNTyMs6lpl4x0tiHL+Wsz2DHCxQlQ==
|
||||||
|
dependencies:
|
||||||
|
moment "^2.10.2"
|
||||||
|
|
||||||
"@types/core-js@^2.5.2":
|
"@types/core-js@^2.5.2":
|
||||||
version "2.5.2"
|
version "2.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/core-js/-/core-js-2.5.2.tgz#d4c25420044d4a5b65e00a82fc04b7824b62691f"
|
resolved "https://registry.yarnpkg.com/@types/core-js/-/core-js-2.5.2.tgz#d4c25420044d4a5b65e00a82fc04b7824b62691f"
|
||||||
|
|
|
@ -166,42 +166,43 @@ export type SummaryOptions = {
|
||||||
VideoModel
|
VideoModel
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
[ScopeNames.WITH_STATS]: (options: AvailableWithStatsOptions = { daysPrior: 30 }) => ({
|
[ScopeNames.WITH_STATS]: (options: AvailableWithStatsOptions = { daysPrior: 30 }) => {
|
||||||
attributes: {
|
const daysPrior = parseInt(options.daysPrior + '', 10)
|
||||||
include: [
|
|
||||||
[
|
return {
|
||||||
literal(
|
attributes: {
|
||||||
'(' +
|
include: [
|
||||||
`SELECT string_agg(concat_ws('|', t.day, t.views), ',') ` +
|
[
|
||||||
'FROM ( ' +
|
literal(
|
||||||
'WITH ' +
|
'(' +
|
||||||
'days AS ( ' +
|
`SELECT string_agg(concat_ws('|', t.day, t.views), ',') ` +
|
||||||
`SELECT generate_series(date_trunc('day', now()) - '${options.daysPrior} day'::interval, ` +
|
'FROM ( ' +
|
||||||
`date_trunc('day', now()), '1 day'::interval) AS day ` +
|
'WITH ' +
|
||||||
'), ' +
|
'days AS ( ' +
|
||||||
'views AS ( ' +
|
`SELECT generate_series(date_trunc('day', now()) - '${daysPrior} day'::interval, ` +
|
||||||
'SELECT * ' +
|
`date_trunc('day', now()), '1 day'::interval) AS day ` +
|
||||||
'FROM "videoView" ' +
|
'), ' +
|
||||||
'WHERE "videoView"."videoId" IN ( ' +
|
'views AS ( ' +
|
||||||
'SELECT "video"."id" ' +
|
'SELECT v.* ' +
|
||||||
'FROM "video" ' +
|
'FROM "videoView" AS v ' +
|
||||||
|
'INNER JOIN "video" ON "video"."id" = v."videoId" ' +
|
||||||
'WHERE "video"."channelId" = "VideoChannelModel"."id" ' +
|
'WHERE "video"."channelId" = "VideoChannelModel"."id" ' +
|
||||||
') ' +
|
') ' +
|
||||||
') ' +
|
'SELECT days.day AS day, ' +
|
||||||
'SELECT days.day AS day, ' +
|
'COALESCE(SUM(views.views), 0) AS views ' +
|
||||||
'COALESCE(SUM(views.views), 0) AS views ' +
|
'FROM days ' +
|
||||||
'FROM days ' +
|
`LEFT JOIN views ON date_trunc('day', "views"."startDate") = date_trunc('day', days.day) ` +
|
||||||
`LEFT JOIN views ON date_trunc('day', "views"."startDate") = date_trunc('day', days.day) ` +
|
'GROUP BY day ' +
|
||||||
'GROUP BY 1 ' +
|
'ORDER BY day ' +
|
||||||
'ORDER BY day ' +
|
') t' +
|
||||||
') t' +
|
')'
|
||||||
')'
|
),
|
||||||
),
|
'viewsPerDay'
|
||||||
'viewsPerDay'
|
]
|
||||||
]
|
]
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}))
|
}))
|
||||||
@Table({
|
@Table({
|
||||||
tableName: 'videoChannel',
|
tableName: 'videoChannel',
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import * as chai from 'chai'
|
import * as chai from 'chai'
|
||||||
import 'mocha'
|
import 'mocha'
|
||||||
import { User, Video, VideoChannel, viewsPerTime, VideoDetails } from '../../../../shared/index'
|
import { User, Video, VideoChannel, ViewsPerDate, VideoDetails } from '../../../../shared/index'
|
||||||
import {
|
import {
|
||||||
cleanupTests,
|
cleanupTests,
|
||||||
createUser,
|
createUser,
|
||||||
|
@ -376,7 +376,7 @@ describe('Test video channels', function () {
|
||||||
res.body.data.forEach((channel: VideoChannel) => {
|
res.body.data.forEach((channel: VideoChannel) => {
|
||||||
expect(channel).to.haveOwnProperty('viewsPerDay')
|
expect(channel).to.haveOwnProperty('viewsPerDay')
|
||||||
expect(channel.viewsPerDay).to.have.length(30 + 1) // daysPrior + today
|
expect(channel.viewsPerDay).to.have.length(30 + 1) // daysPrior + today
|
||||||
channel.viewsPerDay.forEach((v: viewsPerTime) => {
|
channel.viewsPerDay.forEach((v: ViewsPerDate) => {
|
||||||
expect(v.date).to.be.an('string')
|
expect(v.date).to.be.an('string')
|
||||||
expect(v.views).to.equal(0)
|
expect(v.views).to.equal(0)
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Actor } from '../../actors/actor.model'
|
||||||
import { Account } from '../../actors/index'
|
import { Account } from '../../actors/index'
|
||||||
import { Avatar } from '../../avatars'
|
import { Avatar } from '../../avatars'
|
||||||
|
|
||||||
export type viewsPerTime = {
|
export type ViewsPerDate = {
|
||||||
date: Date
|
date: Date
|
||||||
views: number
|
views: number
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ export interface VideoChannel extends Actor {
|
||||||
support: string
|
support: string
|
||||||
isLocal: boolean
|
isLocal: boolean
|
||||||
ownerAccount?: Account
|
ownerAccount?: Account
|
||||||
viewsPerDay?: viewsPerTime[] // chronologically ordered
|
viewsPerDay?: ViewsPerDate[] // chronologically ordered
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VideoChannelSummary {
|
export interface VideoChannelSummary {
|
||||||
|
|
Loading…
Reference in New Issue