From 3d527ba173a37bd61ec8ad742642bb320d12995c Mon Sep 17 00:00:00 2001 From: Rigel Kent Date: Mon, 30 Mar 2020 12:06:46 +0200 Subject: [PATCH] Use inner join and document code for viewr stats for channels --- client/package.json | 1 + .../my-account-video-channels.component.html | 2 +- .../my-account-video-channels.component.ts | 28 ++++++-- .../video-channel/video-channel.model.ts | 4 +- client/yarn.lock | 7 ++ server/models/video/video-channel.ts | 65 ++++++++++--------- server/tests/api/videos/video-channels.ts | 4 +- .../videos/channel/video-channel.model.ts | 4 +- 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 @@ "@ngx-loading-bar/router": "^4.2.0", "@ngx-meta/core": "^8.0.2", "@ngx-translate/i18n-polyfill": "^1.0.0", + "@types/chart.js": "^2.9.16", "@types/core-js": "^2.5.2", "@types/debug": "^4.1.5", "@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 @@
{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}
- +
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' import { User } from '@app/shared' import { flatMap } from 'rxjs/operators' 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({ selector: 'my-account-video-channels', @@ -17,7 +18,7 @@ import { minBy, maxBy } from 'lodash-es' }) export class MyAccountVideoChannelsComponent implements OnInit { videoChannels: VideoChannel[] = [] - videoChannelsData: any[] + videoChannelsChartData: ChartData[] videoChannelsMinimumDailyViews = 0 videoChannelsMaximumDailyViews: number @@ -125,7 +126,9 @@ export class MyAccountVideoChannelsComponent implements OnInit { .pipe(flatMap(() => this.videoChannelService.listAccountVideoChannels(this.user.account, null, true))) .subscribe(res => { 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()), datasets: [ { @@ -135,9 +138,22 @@ export class MyAccountVideoChannelsComponent implements OnInit { borderColor: "#c6c6c6" } ] - })) - 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 + } as ChartData)) + + // 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 + ) }) } } 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 @@ -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 { Account } from '../../../../../shared/models/actors' @@ -12,7 +12,7 @@ export class VideoChannel extends Actor implements ServerVideoChannel { ownerAccount?: Account ownerBy?: string ownerAvatarUrl?: string - viewsPerDay?: viewsPerTime[] + viewsPerDay?: ViewsPerDate[] constructor (hash: ServerVideoChannel) { 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 @@ dependencies: "@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": version "2.5.2" 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 = { VideoModel ] }, - [ScopeNames.WITH_STATS]: (options: AvailableWithStatsOptions = { daysPrior: 30 }) => ({ - attributes: { - include: [ - [ - literal( - '(' + - `SELECT string_agg(concat_ws('|', t.day, t.views), ',') ` + - 'FROM ( ' + - 'WITH ' + - 'days AS ( ' + - `SELECT generate_series(date_trunc('day', now()) - '${options.daysPrior} day'::interval, ` + - `date_trunc('day', now()), '1 day'::interval) AS day ` + - '), ' + - 'views AS ( ' + - 'SELECT * ' + - 'FROM "videoView" ' + - 'WHERE "videoView"."videoId" IN ( ' + - 'SELECT "video"."id" ' + - 'FROM "video" ' + + [ScopeNames.WITH_STATS]: (options: AvailableWithStatsOptions = { daysPrior: 30 }) => { + const daysPrior = parseInt(options.daysPrior + '', 10) + + return { + attributes: { + include: [ + [ + literal( + '(' + + `SELECT string_agg(concat_ws('|', t.day, t.views), ',') ` + + 'FROM ( ' + + 'WITH ' + + 'days AS ( ' + + `SELECT generate_series(date_trunc('day', now()) - '${daysPrior} day'::interval, ` + + `date_trunc('day', now()), '1 day'::interval) AS day ` + + '), ' + + 'views AS ( ' + + 'SELECT v.* ' + + 'FROM "videoView" AS v ' + + 'INNER JOIN "video" ON "video"."id" = v."videoId" ' + 'WHERE "video"."channelId" = "VideoChannelModel"."id" ' + ') ' + - ') ' + - 'SELECT days.day AS day, ' + - 'COALESCE(SUM(views.views), 0) AS views ' + - 'FROM days ' + - `LEFT JOIN views ON date_trunc('day', "views"."startDate") = date_trunc('day', days.day) ` + - 'GROUP BY 1 ' + - 'ORDER BY day ' + - ') t' + - ')' - ), - 'viewsPerDay' + 'SELECT days.day AS day, ' + + 'COALESCE(SUM(views.views), 0) AS views ' + + 'FROM days ' + + `LEFT JOIN views ON date_trunc('day', "views"."startDate") = date_trunc('day', days.day) ` + + 'GROUP BY day ' + + 'ORDER BY day ' + + ') t' + + ')' + ), + 'viewsPerDay' + ] ] - ] + } } - }) + } })) @Table({ 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 @@ import * as chai from 'chai' import 'mocha' -import { User, Video, VideoChannel, viewsPerTime, VideoDetails } from '../../../../shared/index' +import { User, Video, VideoChannel, ViewsPerDate, VideoDetails } from '../../../../shared/index' import { cleanupTests, createUser, @@ -376,7 +376,7 @@ describe('Test video channels', function () { res.body.data.forEach((channel: VideoChannel) => { expect(channel).to.haveOwnProperty('viewsPerDay') 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.views).to.equal(0) }) 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' import { Account } from '../../actors/index' import { Avatar } from '../../avatars' -export type viewsPerTime = { +export type ViewsPerDate = { date: Date views: number } @@ -13,7 +13,7 @@ export interface VideoChannel extends Actor { support: string isLocal: boolean ownerAccount?: Account - viewsPerDay?: viewsPerTime[] // chronologically ordered + viewsPerDay?: ViewsPerDate[] // chronologically ordered } export interface VideoChannelSummary {