diff options
author | Rigel Kent <sendmemail@rigelk.eu> | 2020-03-23 10:14:05 +0100 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2020-03-31 10:29:24 +0200 |
commit | 8165d00ac6263cf3c0d61d450960ef36635084ff (patch) | |
tree | c0587121cd8dbdfc246a5bc74c08805830140a77 | |
parent | 628c155338cf106365a06ca021b9f244b784c003 (diff) | |
download | PeerTube-8165d00ac6263cf3c0d61d450960ef36635084ff.tar.gz PeerTube-8165d00ac6263cf3c0d61d450960ef36635084ff.tar.zst PeerTube-8165d00ac6263cf3c0d61d450960ef36635084ff.zip |
View stats for channels
7 files changed, 212 insertions, 51 deletions
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 2461aa3f5..94e74938b 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 | |||
@@ -6,7 +6,7 @@ | |||
6 | </div> | 6 | </div> |
7 | 7 | ||
8 | <div class="video-channels"> | 8 | <div class="video-channels"> |
9 | <div *ngFor="let videoChannel of videoChannels" class="video-channel"> | 9 | <div *ngFor="let videoChannel of videoChannels; let i = index" class="video-channel"> |
10 | <a [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]"> | 10 | <a [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]"> |
11 | <img [src]="videoChannel.avatarUrl" alt="Avatar" /> | 11 | <img [src]="videoChannel.avatarUrl" alt="Avatar" /> |
12 | </a> | 12 | </a> |
@@ -17,13 +17,16 @@ | |||
17 | <div class="video-channel-name">{{ videoChannel.nameWithHost }}</div> | 17 | <div class="video-channel-name">{{ videoChannel.nameWithHost }}</div> |
18 | </a> | 18 | </a> |
19 | 19 | ||
20 | <div i18n class="video-channel-followers">{{ videoChannel.followersCount }} subscribers</div> | 20 | <div i18n class="video-channel-followers">{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div> |
21 | |||
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> | ||
24 | </div> | ||
21 | </div> | 25 | </div> |
22 | 26 | ||
23 | <div class="video-channel-buttons"> | 27 | <div class="video-channel-buttons"> |
24 | <my-delete-button (click)="deleteVideoChannel(videoChannel)"></my-delete-button> | ||
25 | |||
26 | <my-edit-button [routerLink]="[ 'update', videoChannel.nameWithHost ]"></my-edit-button> | 28 | <my-edit-button [routerLink]="[ 'update', videoChannel.nameWithHost ]"></my-edit-button> |
29 | <my-delete-button (click)="deleteVideoChannel(videoChannel)"></my-delete-button> | ||
27 | </div> | 30 | </div> |
28 | </div> | 31 | </div> |
29 | </div> | 32 | </div> |
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss index db0c7f94f..c0dc41f12 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss | |||
@@ -6,13 +6,14 @@ | |||
6 | } | 6 | } |
7 | 7 | ||
8 | ::ng-deep .action-button { | 8 | ::ng-deep .action-button { |
9 | &.action-button-delete { | 9 | &.action-button-edit { |
10 | margin-right: 10px; | 10 | margin-right: 10px; |
11 | } | 11 | } |
12 | } | 12 | } |
13 | 13 | ||
14 | .video-channel { | 14 | .video-channel { |
15 | @include row-blocks; | 15 | @include row-blocks; |
16 | padding-bottom: 0; | ||
16 | 17 | ||
17 | img { | 18 | img { |
18 | @include avatar(80px); | 19 | @include avatar(80px); |
@@ -58,6 +59,11 @@ | |||
58 | margin: 20px 0 50px; | 59 | margin: 20px 0 50px; |
59 | } | 60 | } |
60 | 61 | ||
62 | ::ng-deep .chartjs-render-monitor { | ||
63 | position: relative; | ||
64 | top: 1px; | ||
65 | } | ||
66 | |||
61 | @media screen and (max-width: $small-view) { | 67 | @media screen and (max-width: $small-view) { |
62 | .video-channels-header { | 68 | .video-channels-header { |
63 | text-align: center; | 69 | text-align: center; |
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 3b01b6c9f..eeab3a8dd 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 | |||
@@ -4,9 +4,11 @@ import { AuthService } from '../../core/auth' | |||
4 | import { ConfirmService } from '../../core/confirm' | 4 | import { ConfirmService } from '../../core/confirm' |
5 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | 5 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' |
6 | import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' | 6 | import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' |
7 | import { ScreenService } from '@app/shared/misc/screen.service' | ||
7 | import { User } from '@app/shared' | 8 | import { User } from '@app/shared' |
8 | import { flatMap } from 'rxjs/operators' | 9 | import { flatMap } from 'rxjs/operators' |
9 | import { I18n } from '@ngx-translate/i18n-polyfill' | 10 | import { I18n } from '@ngx-translate/i18n-polyfill' |
11 | import { minBy, maxBy } from 'lodash-es' | ||
10 | 12 | ||
11 | @Component({ | 13 | @Component({ |
12 | selector: 'my-account-video-channels', | 14 | selector: 'my-account-video-channels', |
@@ -15,6 +17,9 @@ import { I18n } from '@ngx-translate/i18n-polyfill' | |||
15 | }) | 17 | }) |
16 | export class MyAccountVideoChannelsComponent implements OnInit { | 18 | export class MyAccountVideoChannelsComponent implements OnInit { |
17 | videoChannels: VideoChannel[] = [] | 19 | videoChannels: VideoChannel[] = [] |
20 | videoChannelsData: any[] | ||
21 | videoChannelsMinimumDailyViews = 0 | ||
22 | videoChannelsMaximumDailyViews: number | ||
18 | 23 | ||
19 | private user: User | 24 | private user: User |
20 | 25 | ||
@@ -23,6 +28,7 @@ export class MyAccountVideoChannelsComponent implements OnInit { | |||
23 | private notifier: Notifier, | 28 | private notifier: Notifier, |
24 | private confirmService: ConfirmService, | 29 | private confirmService: ConfirmService, |
25 | private videoChannelService: VideoChannelService, | 30 | private videoChannelService: VideoChannelService, |
31 | private screenService: ScreenService, | ||
26 | private i18n: I18n | 32 | private i18n: I18n |
27 | ) {} | 33 | ) {} |
28 | 34 | ||
@@ -32,6 +38,61 @@ export class MyAccountVideoChannelsComponent implements OnInit { | |||
32 | this.loadVideoChannels() | 38 | this.loadVideoChannels() |
33 | } | 39 | } |
34 | 40 | ||
41 | get isInSmallView () { | ||
42 | return this.screenService.isInSmallView() | ||
43 | } | ||
44 | |||
45 | get chartOptions () { | ||
46 | return { | ||
47 | legend: { | ||
48 | display: false | ||
49 | }, | ||
50 | scales: { | ||
51 | xAxes: [{ | ||
52 | display: false | ||
53 | }], | ||
54 | yAxes: [{ | ||
55 | display: false, | ||
56 | ticks: { | ||
57 | min: Math.max(0, this.videoChannelsMinimumDailyViews - (3 * this.videoChannelsMaximumDailyViews / 100)), | ||
58 | max: this.videoChannelsMaximumDailyViews | ||
59 | } | ||
60 | }], | ||
61 | }, | ||
62 | layout: { | ||
63 | padding: { | ||
64 | left: 15, | ||
65 | right: 15, | ||
66 | top: 10, | ||
67 | bottom: 0 | ||
68 | } | ||
69 | }, | ||
70 | elements: { | ||
71 | point:{ | ||
72 | radius: 0 | ||
73 | } | ||
74 | }, | ||
75 | tooltips: { | ||
76 | mode: 'index', | ||
77 | intersect: false, | ||
78 | custom: function (tooltip: any) { | ||
79 | if (!tooltip) return; | ||
80 | // disable displaying the color box; | ||
81 | tooltip.displayColors = false; | ||
82 | }, | ||
83 | callbacks: { | ||
84 | label: function (tooltip: any, data: any) { | ||
85 | return `${tooltip.value} views`; | ||
86 | } | ||
87 | } | ||
88 | }, | ||
89 | hover: { | ||
90 | mode: 'index', | ||
91 | intersect: false | ||
92 | } | ||
93 | } | ||
94 | } | ||
95 | |||
35 | async deleteVideoChannel (videoChannel: VideoChannel) { | 96 | async deleteVideoChannel (videoChannel: VideoChannel) { |
36 | const res = await this.confirmService.confirmWithInput( | 97 | const res = await this.confirmService.confirmWithInput( |
37 | this.i18n( | 98 | this.i18n( |
@@ -64,6 +125,21 @@ export class MyAccountVideoChannelsComponent implements OnInit { | |||
64 | private loadVideoChannels () { | 125 | private loadVideoChannels () { |
65 | this.authService.userInformationLoaded | 126 | this.authService.userInformationLoaded |
66 | .pipe(flatMap(() => this.videoChannelService.listAccountVideoChannels(this.user.account))) | 127 | .pipe(flatMap(() => this.videoChannelService.listAccountVideoChannels(this.user.account))) |
67 | .subscribe(res => this.videoChannels = res.data) | 128 | .subscribe(res => { |
129 | this.videoChannels = res.data | ||
130 | this.videoChannelsData = this.videoChannels.map(v => ({ | ||
131 | labels: v.viewsPerDay.map(day => day.date.toLocaleDateString()), | ||
132 | datasets: [ | ||
133 | { | ||
134 | label: this.i18n('Views for the day'), | ||
135 | data: v.viewsPerDay.map(day => day.views), | ||
136 | fill: false, | ||
137 | borderColor: "#c6c6c6" | ||
138 | } | ||
139 | ] | ||
140 | })) | ||
141 | this.videoChannelsMinimumDailyViews = minBy(this.videoChannels.map(v => minBy(v.viewsPerDay, day => day.views)), day => day.views).views | ||
142 | this.videoChannelsMaximumDailyViews = maxBy(this.videoChannels.map(v => maxBy(v.viewsPerDay, day => day.views)), day => day.views).views | ||
143 | }) | ||
68 | } | 144 | } |
69 | } | 145 | } |
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts index f8c04cb4d..42b61bba6 100644 --- a/client/src/app/+my-account/my-account.module.ts +++ b/client/src/app/+my-account/my-account.module.ts | |||
@@ -1,7 +1,8 @@ | |||
1 | import { TableModule } from 'primeng/table' | ||
2 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { TableModule } from 'primeng/table' | ||
3 | import { AutoCompleteModule } from 'primeng/autocomplete' | 3 | import { AutoCompleteModule } from 'primeng/autocomplete' |
4 | import { InputSwitchModule } from 'primeng/inputswitch' | 4 | import { InputSwitchModule } from 'primeng/inputswitch' |
5 | import { ChartModule } from 'primeng/chart' | ||
5 | import { SharedModule } from '../shared' | 6 | import { SharedModule } from '../shared' |
6 | import { MyAccountRoutingModule } from './my-account-routing.module' | 7 | import { MyAccountRoutingModule } from './my-account-routing.module' |
7 | import { MyAccountChangePasswordComponent } from './my-account-settings/my-account-change-password/my-account-change-password.component' | 8 | import { MyAccountChangePasswordComponent } from './my-account-settings/my-account-change-password/my-account-change-password.component' |
@@ -44,7 +45,8 @@ import { MyAccountChangeEmailComponent } from '@app/+my-account/my-account-setti | |||
44 | SharedModule, | 45 | SharedModule, |
45 | TableModule, | 46 | TableModule, |
46 | InputSwitchModule, | 47 | InputSwitchModule, |
47 | DragDropModule | 48 | DragDropModule, |
49 | ChartModule | ||
48 | ], | 50 | ], |
49 | 51 | ||
50 | declarations: [ | 52 | declarations: [ |
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 fec050cde..ee3288d7a 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 @@ | |||
1 | import { VideoChannel as ServerVideoChannel } from '../../../../../shared/models/videos' | 1 | import { VideoChannel as ServerVideoChannel, viewsPerTime } from '../../../../../shared/models/videos' |
2 | import { Actor } from '../actor/actor.model' | 2 | import { Actor } from '../actor/actor.model' |
3 | import { Account } from '../../../../../shared/models/actors' | 3 | import { Account } from '../../../../../shared/models/actors' |
4 | 4 | ||
@@ -12,6 +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 | 16 | ||
16 | constructor (hash: ServerVideoChannel) { | 17 | constructor (hash: ServerVideoChannel) { |
17 | super(hash) | 18 | super(hash) |
@@ -23,6 +24,10 @@ export class VideoChannel extends Actor implements ServerVideoChannel { | |||
23 | this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host) | 24 | this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host) |
24 | this.nameWithHostForced = Actor.CREATE_BY_STRING(this.name, this.host, true) | 25 | this.nameWithHostForced = Actor.CREATE_BY_STRING(this.name, this.host, true) |
25 | 26 | ||
27 | if (hash.viewsPerDay) { | ||
28 | this.viewsPerDay = hash.viewsPerDay.map(v => ({ ...v, date: new Date(v.date)})) | ||
29 | } | ||
30 | |||
26 | if (hash.ownerAccount) { | 31 | if (hash.ownerAccount) { |
27 | this.ownerAccount = hash.ownerAccount | 32 | this.ownerAccount = hash.ownerAccount |
28 | this.ownerBy = Actor.CREATE_BY_STRING(hash.ownerAccount.name, hash.ownerAccount.host) | 33 | this.ownerBy = Actor.CREATE_BY_STRING(hash.ownerAccount.name, hash.ownerAccount.host) |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 835216671..128915af3 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -30,7 +30,7 @@ import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttr | |||
30 | import { VideoModel } from './video' | 30 | import { VideoModel } from './video' |
31 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' | 31 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' |
32 | import { ServerModel } from '../server/server' | 32 | import { ServerModel } from '../server/server' |
33 | import { FindOptions, Op } from 'sequelize' | 33 | import { FindOptions, Op, literal, ScopeOptions } from 'sequelize' |
34 | import { AvatarModel } from '../avatar/avatar' | 34 | import { AvatarModel } from '../avatar/avatar' |
35 | import { VideoPlaylistModel } from './video-playlist' | 35 | import { VideoPlaylistModel } from './video-playlist' |
36 | import * as Bluebird from 'bluebird' | 36 | import * as Bluebird from 'bluebird' |
@@ -45,16 +45,21 @@ import { | |||
45 | 45 | ||
46 | export enum ScopeNames { | 46 | export enum ScopeNames { |
47 | FOR_API = 'FOR_API', | 47 | FOR_API = 'FOR_API', |
48 | SUMMARY = 'SUMMARY', | ||
48 | WITH_ACCOUNT = 'WITH_ACCOUNT', | 49 | WITH_ACCOUNT = 'WITH_ACCOUNT', |
49 | WITH_ACTOR = 'WITH_ACTOR', | 50 | WITH_ACTOR = 'WITH_ACTOR', |
50 | WITH_VIDEOS = 'WITH_VIDEOS', | 51 | WITH_VIDEOS = 'WITH_VIDEOS', |
51 | SUMMARY = 'SUMMARY' | 52 | WITH_STATS = 'WITH_STATS' |
52 | } | 53 | } |
53 | 54 | ||
54 | type AvailableForListOptions = { | 55 | type AvailableForListOptions = { |
55 | actorId: number | 56 | actorId: number |
56 | } | 57 | } |
57 | 58 | ||
59 | type AvailableWithStatsOptions = { | ||
60 | daysPrior: number | ||
61 | } | ||
62 | |||
58 | export type SummaryOptions = { | 63 | export type SummaryOptions = { |
59 | withAccount?: boolean // Default: false | 64 | withAccount?: boolean // Default: false |
60 | withAccountBlockerIds?: number[] | 65 | withAccountBlockerIds?: number[] |
@@ -69,40 +74,6 @@ export type SummaryOptions = { | |||
69 | ] | 74 | ] |
70 | })) | 75 | })) |
71 | @Scopes(() => ({ | 76 | @Scopes(() => ({ |
72 | [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => { | ||
73 | const base: FindOptions = { | ||
74 | attributes: [ 'id', 'name', 'description', 'actorId' ], | ||
75 | include: [ | ||
76 | { | ||
77 | attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], | ||
78 | model: ActorModel.unscoped(), | ||
79 | required: true, | ||
80 | include: [ | ||
81 | { | ||
82 | attributes: [ 'host' ], | ||
83 | model: ServerModel.unscoped(), | ||
84 | required: false | ||
85 | }, | ||
86 | { | ||
87 | model: AvatarModel.unscoped(), | ||
88 | required: false | ||
89 | } | ||
90 | ] | ||
91 | } | ||
92 | ] | ||
93 | } | ||
94 | |||
95 | if (options.withAccount === true) { | ||
96 | base.include.push({ | ||
97 | model: AccountModel.scope({ | ||
98 | method: [ AccountModelScopeNames.SUMMARY, { withAccountBlockerIds: options.withAccountBlockerIds } as AccountSummaryOptions ] | ||
99 | }), | ||
100 | required: true | ||
101 | }) | ||
102 | } | ||
103 | |||
104 | return base | ||
105 | }, | ||
106 | [ScopeNames.FOR_API]: (options: AvailableForListOptions) => { | 77 | [ScopeNames.FOR_API]: (options: AvailableForListOptions) => { |
107 | // Only list local channels OR channels that are on an instance followed by actorId | 78 | // Only list local channels OR channels that are on an instance followed by actorId |
108 | const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId) | 79 | const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId) |
@@ -143,6 +114,40 @@ export type SummaryOptions = { | |||
143 | ] | 114 | ] |
144 | } | 115 | } |
145 | }, | 116 | }, |
117 | [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => { | ||
118 | const base: FindOptions = { | ||
119 | attributes: [ 'id', 'name', 'description', 'actorId' ], | ||
120 | include: [ | ||
121 | { | ||
122 | attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], | ||
123 | model: ActorModel.unscoped(), | ||
124 | required: true, | ||
125 | include: [ | ||
126 | { | ||
127 | attributes: [ 'host' ], | ||
128 | model: ServerModel.unscoped(), | ||
129 | required: false | ||
130 | }, | ||
131 | { | ||
132 | model: AvatarModel.unscoped(), | ||
133 | required: false | ||
134 | } | ||
135 | ] | ||
136 | } | ||
137 | ] | ||
138 | } | ||
139 | |||
140 | if (options.withAccount === true) { | ||
141 | base.include.push({ | ||
142 | model: AccountModel.scope({ | ||
143 | method: [ AccountModelScopeNames.SUMMARY, { withAccountBlockerIds: options.withAccountBlockerIds } as AccountSummaryOptions ] | ||
144 | }), | ||
145 | required: true | ||
146 | }) | ||
147 | } | ||
148 | |||
149 | return base | ||
150 | }, | ||
146 | [ScopeNames.WITH_ACCOUNT]: { | 151 | [ScopeNames.WITH_ACCOUNT]: { |
147 | include: [ | 152 | include: [ |
148 | { | 153 | { |
@@ -151,16 +156,52 @@ export type SummaryOptions = { | |||
151 | } | 156 | } |
152 | ] | 157 | ] |
153 | }, | 158 | }, |
154 | [ScopeNames.WITH_VIDEOS]: { | 159 | [ScopeNames.WITH_ACTOR]: { |
155 | include: [ | 160 | include: [ |
156 | VideoModel | 161 | ActorModel |
157 | ] | 162 | ] |
158 | }, | 163 | }, |
159 | [ScopeNames.WITH_ACTOR]: { | 164 | [ScopeNames.WITH_VIDEOS]: { |
160 | include: [ | 165 | include: [ |
161 | ActorModel | 166 | VideoModel |
162 | ] | 167 | ] |
163 | } | 168 | }, |
169 | [ScopeNames.WITH_STATS]: (options: AvailableWithStatsOptions = { daysPrior: 30 }) => ({ | ||
170 | attributes: { | ||
171 | include: [ | ||
172 | [ | ||
173 | literal( | ||
174 | '(' + | ||
175 | `SELECT string_agg(concat_ws('|', t.day, t.views), ',') ` + | ||
176 | 'FROM ( ' + | ||
177 | 'WITH ' + | ||
178 | 'days AS ( ' + | ||
179 | `SELECT generate_series(date_trunc('day', now()) - '${options.daysPrior} day'::interval, ` + | ||
180 | `date_trunc('day', now()), '1 day'::interval) AS day ` + | ||
181 | '), ' + | ||
182 | 'views AS ( ' + | ||
183 | 'SELECT * ' + | ||
184 | 'FROM "videoView" ' + | ||
185 | 'WHERE "videoView"."videoId" IN ( ' + | ||
186 | 'SELECT "video"."id" ' + | ||
187 | 'FROM "video" ' + | ||
188 | 'WHERE "video"."channelId" = "VideoChannelModel"."id" ' + | ||
189 | ') ' + | ||
190 | ') ' + | ||
191 | 'SELECT days.day AS day, ' + | ||
192 | 'COALESCE(SUM(views.views), 0) AS views ' + | ||
193 | 'FROM days ' + | ||
194 | `LEFT JOIN views ON date_trunc('day', "views"."createdAt") = days.day ` + | ||
195 | 'GROUP BY 1 ' + | ||
196 | 'ORDER BY day ' + | ||
197 | ') t' + | ||
198 | ')' | ||
199 | ), | ||
200 | 'viewsPerDay' | ||
201 | ] | ||
202 | ] | ||
203 | } | ||
204 | }) | ||
164 | })) | 205 | })) |
165 | @Table({ | 206 | @Table({ |
166 | tableName: 'videoChannel', | 207 | tableName: 'videoChannel', |
@@ -352,6 +393,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> { | |||
352 | start: number | 393 | start: number |
353 | count: number | 394 | count: number |
354 | sort: string | 395 | sort: string |
396 | withStats?: boolean | ||
355 | }) { | 397 | }) { |
356 | const query = { | 398 | const query = { |
357 | offset: options.start, | 399 | offset: options.start, |
@@ -368,7 +410,17 @@ export class VideoChannelModel extends Model<VideoChannelModel> { | |||
368 | ] | 410 | ] |
369 | } | 411 | } |
370 | 412 | ||
413 | const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR ] | ||
414 | |||
415 | options.withStats = true // TODO: remove beyond after initial tests | ||
416 | if (options.withStats) { | ||
417 | scopes.push({ | ||
418 | method: [ ScopeNames.WITH_STATS, { daysPrior: 30 } as AvailableWithStatsOptions ] | ||
419 | }) | ||
420 | } | ||
421 | |||
371 | return VideoChannelModel | 422 | return VideoChannelModel |
423 | .scope(scopes) | ||
372 | .findAndCountAll(query) | 424 | .findAndCountAll(query) |
373 | .then(({ rows, count }) => { | 425 | .then(({ rows, count }) => { |
374 | return { total: count, data: rows } | 426 | return { total: count, data: rows } |
@@ -496,6 +548,8 @@ export class VideoChannelModel extends Model<VideoChannelModel> { | |||
496 | } | 548 | } |
497 | 549 | ||
498 | toFormattedJSON (this: MChannelFormattable): VideoChannel { | 550 | toFormattedJSON (this: MChannelFormattable): VideoChannel { |
551 | const viewsPerDay = this.get('viewsPerDay') as string | ||
552 | |||
499 | const actor = this.Actor.toFormattedJSON() | 553 | const actor = this.Actor.toFormattedJSON() |
500 | const videoChannel = { | 554 | const videoChannel = { |
501 | id: this.id, | 555 | id: this.id, |
@@ -505,7 +559,16 @@ export class VideoChannelModel extends Model<VideoChannelModel> { | |||
505 | isLocal: this.Actor.isOwned(), | 559 | isLocal: this.Actor.isOwned(), |
506 | createdAt: this.createdAt, | 560 | createdAt: this.createdAt, |
507 | updatedAt: this.updatedAt, | 561 | updatedAt: this.updatedAt, |
508 | ownerAccount: undefined | 562 | ownerAccount: undefined, |
563 | viewsPerDay: viewsPerDay !== undefined | ||
564 | ? viewsPerDay.split(',').map(v => { | ||
565 | const o = v.split('|') | ||
566 | return { | ||
567 | date: new Date(o[0]), | ||
568 | views: +o[1] | ||
569 | } | ||
570 | }) | ||
571 | : undefined | ||
509 | } | 572 | } |
510 | 573 | ||
511 | if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON() | 574 | if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON() |
diff --git a/shared/models/videos/channel/video-channel.model.ts b/shared/models/videos/channel/video-channel.model.ts index de4c26b3d..5fe6609d9 100644 --- a/shared/models/videos/channel/video-channel.model.ts +++ b/shared/models/videos/channel/video-channel.model.ts | |||
@@ -2,12 +2,18 @@ import { Actor } from '../../actors/actor.model' | |||
2 | import { Account } from '../../actors/index' | 2 | import { Account } from '../../actors/index' |
3 | import { Avatar } from '../../avatars' | 3 | import { Avatar } from '../../avatars' |
4 | 4 | ||
5 | export type viewsPerTime = { | ||
6 | date: Date | ||
7 | views: number | ||
8 | } | ||
9 | |||
5 | export interface VideoChannel extends Actor { | 10 | export interface VideoChannel extends Actor { |
6 | displayName: string | 11 | displayName: string |
7 | description: string | 12 | description: string |
8 | support: string | 13 | support: string |
9 | isLocal: boolean | 14 | isLocal: boolean |
10 | ownerAccount?: Account | 15 | ownerAccount?: Account |
16 | viewsPerDay?: viewsPerTime[] // chronologically ordered | ||
11 | } | 17 | } |
12 | 18 | ||
13 | export interface VideoChannelSummary { | 19 | export interface VideoChannelSummary { |