aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRigel Kent <sendmemail@rigelk.eu>2020-03-30 12:06:46 +0200
committerChocobozzz <chocobozzz@cpy.re>2020-03-31 10:29:24 +0200
commit3d527ba173a37bd61ec8ad742642bb320d12995c (patch)
tree4b2890df00b64ff6cdcf96afb652af8abcac3ff5
parent714bfcc556177dce2b65a1e58babdf2488e9de13 (diff)
downloadPeerTube-3d527ba173a37bd61ec8ad742642bb320d12995c.tar.gz
PeerTube-3d527ba173a37bd61ec8ad742642bb320d12995c.tar.zst
PeerTube-3d527ba173a37bd61ec8ad742642bb320d12995c.zip
Use inner join and document code for viewr stats for channels
-rw-r--r--client/package.json1
-rw-r--r--client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html2
-rw-r--r--client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.ts28
-rw-r--r--client/src/app/shared/video-channel/video-channel.model.ts4
-rw-r--r--client/yarn.lock7
-rw-r--r--server/models/video/video-channel.ts65
-rw-r--r--server/tests/api/videos/video-channels.ts4
-rw-r--r--shared/models/videos/channel/video-channel.model.ts4
8 files changed, 70 insertions, 45 deletions
diff --git a/client/package.json b/client/package.json
index c6a5fa1bb..52647ce1d 100644
--- a/client/package.json
+++ b/client/package.json
@@ -56,6 +56,7 @@
56 "@ngx-loading-bar/router": "^4.2.0", 56 "@ngx-loading-bar/router": "^4.2.0",
57 "@ngx-meta/core": "^8.0.2", 57 "@ngx-meta/core": "^8.0.2",
58 "@ngx-translate/i18n-polyfill": "^1.0.0", 58 "@ngx-translate/i18n-polyfill": "^1.0.0",
59 "@types/chart.js": "^2.9.16",
59 "@types/core-js": "^2.5.2", 60 "@types/core-js": "^2.5.2",
60 "@types/debug": "^4.1.5", 61 "@types/debug": "^4.1.5",
61 "@types/hls.js": "^0.12.4", 62 "@types/hls.js": "^0.12.4",
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html
index 94e74938b..03d45227e 100644
--- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html
+++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html
@@ -20,7 +20,7 @@
20 <div i18n class="video-channel-followers">{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div> 20 <div i18n class="video-channel-followers">{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div>
21 21
22 <div *ngIf="!isInSmallView" class="w-100 d-flex justify-content-end"> 22 <div *ngIf="!isInSmallView" class="w-100 d-flex justify-content-end">
23 <p-chart *ngIf="videoChannelsData && videoChannelsData[i]" type="line" [data]="videoChannelsData[i]" [options]="chartOptions" width="40vw" height="100px"></p-chart> 23 <p-chart *ngIf="videoChannelsChartData && videoChannelsChartData[i]" type="line" [data]="videoChannelsChartData[i]" [options]="chartOptions" width="40vw" height="100px"></p-chart>
24 </div> 24 </div>
25 </div> 25 </div>
26 26
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.ts b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.ts
index 27a157621..153fc0127 100644
--- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.ts
+++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.ts
@@ -8,7 +8,8 @@ import { ScreenService } from '@app/shared/misc/screen.service'
8import { User } from '@app/shared' 8import { User } from '@app/shared'
9import { flatMap } from 'rxjs/operators' 9import { flatMap } from 'rxjs/operators'
10import { I18n } from '@ngx-translate/i18n-polyfill' 10import { I18n } from '@ngx-translate/i18n-polyfill'
11import { minBy, maxBy } from 'lodash-es' 11import { min, minBy, max, maxBy } from 'lodash-es'
12import { ChartData } from 'chart.js'
12 13
13@Component({ 14@Component({
14 selector: 'my-account-video-channels', 15 selector: 'my-account-video-channels',
@@ -17,7 +18,7 @@ import { minBy, maxBy } from 'lodash-es'
17}) 18})
18export class MyAccountVideoChannelsComponent implements OnInit { 19export class MyAccountVideoChannelsComponent implements OnInit {
19 videoChannels: VideoChannel[] = [] 20 videoChannels: VideoChannel[] = []
20 videoChannelsData: any[] 21 videoChannelsChartData: ChartData[]
21 videoChannelsMinimumDailyViews = 0 22 videoChannelsMinimumDailyViews = 0
22 videoChannelsMaximumDailyViews: number 23 videoChannelsMaximumDailyViews: number
23 24
@@ -125,7 +126,9 @@ export class MyAccountVideoChannelsComponent implements OnInit {
125 .pipe(flatMap(() => this.videoChannelService.listAccountVideoChannels(this.user.account, null, true))) 126 .pipe(flatMap(() => this.videoChannelService.listAccountVideoChannels(this.user.account, null, true)))
126 .subscribe(res => { 127 .subscribe(res => {
127 this.videoChannels = res.data 128 this.videoChannels = res.data
128 this.videoChannelsData = this.videoChannels.map(v => ({ 129
130 // chart data
131 this.videoChannelsChartData = this.videoChannels.map(v => ({
129 labels: v.viewsPerDay.map(day => day.date.toLocaleDateString()), 132 labels: v.viewsPerDay.map(day => day.date.toLocaleDateString()),
130 datasets: [ 133 datasets: [
131 { 134 {
@@ -135,9 +138,22 @@ export class MyAccountVideoChannelsComponent implements OnInit {
135 borderColor: "#c6c6c6" 138 borderColor: "#c6c6c6"
136 } 139 }
137 ] 140 ]
138 })) 141 } as ChartData))
139 this.videoChannelsMinimumDailyViews = minBy(this.videoChannels.map(v => minBy(v.viewsPerDay, day => day.views)), day => day.views).views 142
140 this.videoChannelsMaximumDailyViews = maxBy(this.videoChannels.map(v => maxBy(v.viewsPerDay, day => day.views)), day => day.views).views 143 // chart options that depend on chart data:
144 // we don't want to skew values and have min at 0, so we define what the floor/ceiling is here
145 this.videoChannelsMinimumDailyViews = min(
146 this.videoChannels.map(v => minBy( // compute local minimum daily views for each channel, by their "views" attribute
147 v.viewsPerDay,
148 day => day.views
149 ).views) // the object returned is a ViewPerDate, so we still need to get the views attribute
150 )
151 this.videoChannelsMaximumDailyViews = max(
152 this.videoChannels.map(v => maxBy( // compute local maximum daily views for each channel, by their "views" attribute
153 v.viewsPerDay,
154 day => day.views
155 ).views) // the object returned is a ViewPerDate, so we still need to get the views attribute
156 )
141 }) 157 })
142 } 158 }
143} 159}
diff --git a/client/src/app/shared/video-channel/video-channel.model.ts b/client/src/app/shared/video-channel/video-channel.model.ts
index c93af0ca5..617d6d44d 100644
--- a/client/src/app/shared/video-channel/video-channel.model.ts
+++ b/client/src/app/shared/video-channel/video-channel.model.ts
@@ -1,4 +1,4 @@
1import { VideoChannel as ServerVideoChannel, viewsPerTime } from '../../../../../shared/models/videos' 1import { VideoChannel as ServerVideoChannel, ViewsPerDate } from '../../../../../shared/models/videos'
2import { Actor } from '../actor/actor.model' 2import { Actor } from '../actor/actor.model'
3import { Account } from '../../../../../shared/models/actors' 3import { Account } from '../../../../../shared/models/actors'
4 4
@@ -12,7 +12,7 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
12 ownerAccount?: Account 12 ownerAccount?: Account
13 ownerBy?: string 13 ownerBy?: string
14 ownerAvatarUrl?: string 14 ownerAvatarUrl?: string
15 viewsPerDay?: viewsPerTime[] 15 viewsPerDay?: ViewsPerDate[]
16 16
17 constructor (hash: ServerVideoChannel) { 17 constructor (hash: ServerVideoChannel) {
18 super(hash) 18 super(hash)
diff --git a/client/yarn.lock b/client/yarn.lock
index b3f38a664..e34da3d6e 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -1149,6 +1149,13 @@
1149 dependencies: 1149 dependencies:
1150 "@types/node" "*" 1150 "@types/node" "*"
1151 1151
1152"@types/chart.js@^2.9.16":
1153 version "2.9.16"
1154 resolved "https://registry.yarnpkg.com/@types/chart.js/-/chart.js-2.9.16.tgz#ac9d268fa192c0ec0efd740f802683e3ed97642c"
1155 integrity sha512-Mofg7xFIeAWME46YMVKHPCyUz2Z0KsVMNE1f4oF3T74mK3RiPQxOm9qzoeNTyMs6lpl4x0tiHL+Wsz2DHCxQlQ==
1156 dependencies:
1157 moment "^2.10.2"
1158
1152"@types/core-js@^2.5.2": 1159"@types/core-js@^2.5.2":
1153 version "2.5.2" 1160 version "2.5.2"
1154 resolved "https://registry.yarnpkg.com/@types/core-js/-/core-js-2.5.2.tgz#d4c25420044d4a5b65e00a82fc04b7824b62691f" 1161 resolved "https://registry.yarnpkg.com/@types/core-js/-/core-js-2.5.2.tgz#d4c25420044d4a5b65e00a82fc04b7824b62691f"
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index 78fc3d7e4..642e129ff 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -166,42 +166,43 @@ export type SummaryOptions = {
166 VideoModel 166 VideoModel
167 ] 167 ]
168 }, 168 },
169 [ScopeNames.WITH_STATS]: (options: AvailableWithStatsOptions = { daysPrior: 30 }) => ({ 169 [ScopeNames.WITH_STATS]: (options: AvailableWithStatsOptions = { daysPrior: 30 }) => {
170 attributes: { 170 const daysPrior = parseInt(options.daysPrior + '', 10)
171 include: [ 171
172 [ 172 return {
173 literal( 173 attributes: {
174 '(' + 174 include: [
175 `SELECT string_agg(concat_ws('|', t.day, t.views), ',') ` + 175 [
176 'FROM ( ' + 176 literal(
177 'WITH ' + 177 '(' +
178 'days AS ( ' + 178 `SELECT string_agg(concat_ws('|', t.day, t.views), ',') ` +
179 `SELECT generate_series(date_trunc('day', now()) - '${options.daysPrior} day'::interval, ` + 179 'FROM ( ' +
180 `date_trunc('day', now()), '1 day'::interval) AS day ` + 180 'WITH ' +
181 '), ' + 181 'days AS ( ' +
182 'views AS ( ' + 182 `SELECT generate_series(date_trunc('day', now()) - '${daysPrior} day'::interval, ` +
183 'SELECT * ' + 183 `date_trunc('day', now()), '1 day'::interval) AS day ` +
184 'FROM "videoView" ' + 184 '), ' +
185 'WHERE "videoView"."videoId" IN ( ' + 185 'views AS ( ' +
186 'SELECT "video"."id" ' + 186 'SELECT v.* ' +
187 'FROM "video" ' + 187 'FROM "videoView" AS v ' +
188 'INNER JOIN "video" ON "video"."id" = v."videoId" ' +
188 'WHERE "video"."channelId" = "VideoChannelModel"."id" ' + 189 'WHERE "video"."channelId" = "VideoChannelModel"."id" ' +
189 ') ' + 190 ') ' +
190 ') ' + 191 'SELECT days.day AS day, ' +
191 'SELECT days.day AS day, ' + 192 'COALESCE(SUM(views.views), 0) AS views ' +
192 'COALESCE(SUM(views.views), 0) AS views ' + 193 'FROM days ' +
193 'FROM days ' + 194 `LEFT JOIN views ON date_trunc('day', "views"."startDate") = date_trunc('day', days.day) ` +
194 `LEFT JOIN views ON date_trunc('day', "views"."startDate") = date_trunc('day', days.day) ` + 195 'GROUP BY day ' +
195 'GROUP BY 1 ' + 196 'ORDER BY day ' +
196 'ORDER BY day ' + 197 ') t' +
197 ') t' + 198 ')'
198 ')' 199 ),
199 ), 200 'viewsPerDay'
200 'viewsPerDay' 201 ]
201 ] 202 ]
202 ] 203 }
203 } 204 }
204 }) 205 }
205})) 206}))
206@Table({ 207@Table({
207 tableName: 'videoChannel', 208 tableName: 'videoChannel',
diff --git a/server/tests/api/videos/video-channels.ts b/server/tests/api/videos/video-channels.ts
index bde45584d..876a6ab66 100644
--- a/server/tests/api/videos/video-channels.ts
+++ b/server/tests/api/videos/video-channels.ts
@@ -2,7 +2,7 @@
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { User, Video, VideoChannel, viewsPerTime, VideoDetails } from '../../../../shared/index' 5import { User, Video, VideoChannel, ViewsPerDate, VideoDetails } from '../../../../shared/index'
6import { 6import {
7 cleanupTests, 7 cleanupTests,
8 createUser, 8 createUser,
@@ -376,7 +376,7 @@ describe('Test video channels', function () {
376 res.body.data.forEach((channel: VideoChannel) => { 376 res.body.data.forEach((channel: VideoChannel) => {
377 expect(channel).to.haveOwnProperty('viewsPerDay') 377 expect(channel).to.haveOwnProperty('viewsPerDay')
378 expect(channel.viewsPerDay).to.have.length(30 + 1) // daysPrior + today 378 expect(channel.viewsPerDay).to.have.length(30 + 1) // daysPrior + today
379 channel.viewsPerDay.forEach((v: viewsPerTime) => { 379 channel.viewsPerDay.forEach((v: ViewsPerDate) => {
380 expect(v.date).to.be.an('string') 380 expect(v.date).to.be.an('string')
381 expect(v.views).to.equal(0) 381 expect(v.views).to.equal(0)
382 }) 382 })
diff --git a/shared/models/videos/channel/video-channel.model.ts b/shared/models/videos/channel/video-channel.model.ts
index 5fe6609d9..421004e68 100644
--- a/shared/models/videos/channel/video-channel.model.ts
+++ b/shared/models/videos/channel/video-channel.model.ts
@@ -2,7 +2,7 @@ import { Actor } from '../../actors/actor.model'
2import { Account } from '../../actors/index' 2import { Account } from '../../actors/index'
3import { Avatar } from '../../avatars' 3import { Avatar } from '../../avatars'
4 4
5export type viewsPerTime = { 5export type ViewsPerDate = {
6 date: Date 6 date: Date
7 views: number 7 views: number
8} 8}
@@ -13,7 +13,7 @@ export interface VideoChannel extends Actor {
13 support: string 13 support: string
14 isLocal: boolean 14 isLocal: boolean
15 ownerAccount?: Account 15 ownerAccount?: Account
16 viewsPerDay?: viewsPerTime[] // chronologically ordered 16 viewsPerDay?: ViewsPerDate[] // chronologically ordered
17} 17}
18 18
19export interface VideoChannelSummary { 19export interface VideoChannelSummary {