View stats for channels
This commit is contained in:
parent
628c155338
commit
8165d00ac6
|
@ -6,7 +6,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="video-channels">
|
<div class="video-channels">
|
||||||
<div *ngFor="let videoChannel of videoChannels" class="video-channel">
|
<div *ngFor="let videoChannel of videoChannels; let i = index" class="video-channel">
|
||||||
<a [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]">
|
<a [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]">
|
||||||
<img [src]="videoChannel.avatarUrl" alt="Avatar" />
|
<img [src]="videoChannel.avatarUrl" alt="Avatar" />
|
||||||
</a>
|
</a>
|
||||||
|
@ -17,13 +17,16 @@
|
||||||
<div class="video-channel-name">{{ videoChannel.nameWithHost }}</div>
|
<div class="video-channel-name">{{ videoChannel.nameWithHost }}</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div i18n class="video-channel-followers">{{ 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">
|
||||||
|
<p-chart *ngIf="videoChannelsData && videoChannelsData[i]" type="line" [data]="videoChannelsData[i]" [options]="chartOptions" width="40vw" height="100px"></p-chart>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="video-channel-buttons">
|
<div class="video-channel-buttons">
|
||||||
<my-delete-button (click)="deleteVideoChannel(videoChannel)"></my-delete-button>
|
|
||||||
|
|
||||||
<my-edit-button [routerLink]="[ 'update', videoChannel.nameWithHost ]"></my-edit-button>
|
<my-edit-button [routerLink]="[ 'update', videoChannel.nameWithHost ]"></my-edit-button>
|
||||||
|
<my-delete-button (click)="deleteVideoChannel(videoChannel)"></my-delete-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,13 +6,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep .action-button {
|
::ng-deep .action-button {
|
||||||
&.action-button-delete {
|
&.action-button-edit {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-channel {
|
.video-channel {
|
||||||
@include row-blocks;
|
@include row-blocks;
|
||||||
|
padding-bottom: 0;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
@include avatar(80px);
|
@include avatar(80px);
|
||||||
|
@ -58,6 +59,11 @@
|
||||||
margin: 20px 0 50px;
|
margin: 20px 0 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::ng-deep .chartjs-render-monitor {
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: $small-view) {
|
@media screen and (max-width: $small-view) {
|
||||||
.video-channels-header {
|
.video-channels-header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
@ -4,9 +4,11 @@ import { AuthService } from '../../core/auth'
|
||||||
import { ConfirmService } from '../../core/confirm'
|
import { ConfirmService } from '../../core/confirm'
|
||||||
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
|
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
|
||||||
import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
|
import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
|
||||||
|
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'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-account-video-channels',
|
selector: 'my-account-video-channels',
|
||||||
|
@ -15,6 +17,9 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
})
|
})
|
||||||
export class MyAccountVideoChannelsComponent implements OnInit {
|
export class MyAccountVideoChannelsComponent implements OnInit {
|
||||||
videoChannels: VideoChannel[] = []
|
videoChannels: VideoChannel[] = []
|
||||||
|
videoChannelsData: any[]
|
||||||
|
videoChannelsMinimumDailyViews = 0
|
||||||
|
videoChannelsMaximumDailyViews: number
|
||||||
|
|
||||||
private user: User
|
private user: User
|
||||||
|
|
||||||
|
@ -23,6 +28,7 @@ export class MyAccountVideoChannelsComponent implements OnInit {
|
||||||
private notifier: Notifier,
|
private notifier: Notifier,
|
||||||
private confirmService: ConfirmService,
|
private confirmService: ConfirmService,
|
||||||
private videoChannelService: VideoChannelService,
|
private videoChannelService: VideoChannelService,
|
||||||
|
private screenService: ScreenService,
|
||||||
private i18n: I18n
|
private i18n: I18n
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@ -32,6 +38,61 @@ export class MyAccountVideoChannelsComponent implements OnInit {
|
||||||
this.loadVideoChannels()
|
this.loadVideoChannels()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isInSmallView () {
|
||||||
|
return this.screenService.isInSmallView()
|
||||||
|
}
|
||||||
|
|
||||||
|
get chartOptions () {
|
||||||
|
return {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
xAxes: [{
|
||||||
|
display: false
|
||||||
|
}],
|
||||||
|
yAxes: [{
|
||||||
|
display: false,
|
||||||
|
ticks: {
|
||||||
|
min: Math.max(0, this.videoChannelsMinimumDailyViews - (3 * this.videoChannelsMaximumDailyViews / 100)),
|
||||||
|
max: this.videoChannelsMaximumDailyViews
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
padding: {
|
||||||
|
left: 15,
|
||||||
|
right: 15,
|
||||||
|
top: 10,
|
||||||
|
bottom: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
point:{
|
||||||
|
radius: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltips: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
custom: function (tooltip: any) {
|
||||||
|
if (!tooltip) return;
|
||||||
|
// disable displaying the color box;
|
||||||
|
tooltip.displayColors = false;
|
||||||
|
},
|
||||||
|
callbacks: {
|
||||||
|
label: function (tooltip: any, data: any) {
|
||||||
|
return `${tooltip.value} views`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async deleteVideoChannel (videoChannel: VideoChannel) {
|
async deleteVideoChannel (videoChannel: VideoChannel) {
|
||||||
const res = await this.confirmService.confirmWithInput(
|
const res = await this.confirmService.confirmWithInput(
|
||||||
this.i18n(
|
this.i18n(
|
||||||
|
@ -64,6 +125,21 @@ export class MyAccountVideoChannelsComponent implements OnInit {
|
||||||
private loadVideoChannels () {
|
private loadVideoChannels () {
|
||||||
this.authService.userInformationLoaded
|
this.authService.userInformationLoaded
|
||||||
.pipe(flatMap(() => this.videoChannelService.listAccountVideoChannels(this.user.account)))
|
.pipe(flatMap(() => this.videoChannelService.listAccountVideoChannels(this.user.account)))
|
||||||
.subscribe(res => this.videoChannels = res.data)
|
.subscribe(res => {
|
||||||
|
this.videoChannels = res.data
|
||||||
|
this.videoChannelsData = this.videoChannels.map(v => ({
|
||||||
|
labels: v.viewsPerDay.map(day => day.date.toLocaleDateString()),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: this.i18n('Views for the day'),
|
||||||
|
data: v.viewsPerDay.map(day => day.views),
|
||||||
|
fill: false,
|
||||||
|
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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { TableModule } from 'primeng/table'
|
|
||||||
import { NgModule } from '@angular/core'
|
import { NgModule } from '@angular/core'
|
||||||
|
import { TableModule } from 'primeng/table'
|
||||||
import { AutoCompleteModule } from 'primeng/autocomplete'
|
import { AutoCompleteModule } from 'primeng/autocomplete'
|
||||||
import { InputSwitchModule } from 'primeng/inputswitch'
|
import { InputSwitchModule } from 'primeng/inputswitch'
|
||||||
|
import { ChartModule } from 'primeng/chart'
|
||||||
import { SharedModule } from '../shared'
|
import { SharedModule } from '../shared'
|
||||||
import { MyAccountRoutingModule } from './my-account-routing.module'
|
import { MyAccountRoutingModule } from './my-account-routing.module'
|
||||||
import { MyAccountChangePasswordComponent } from './my-account-settings/my-account-change-password/my-account-change-password.component'
|
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
|
||||||
SharedModule,
|
SharedModule,
|
||||||
TableModule,
|
TableModule,
|
||||||
InputSwitchModule,
|
InputSwitchModule,
|
||||||
DragDropModule
|
DragDropModule,
|
||||||
|
ChartModule
|
||||||
],
|
],
|
||||||
|
|
||||||
declarations: [
|
declarations: [
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { VideoChannel as ServerVideoChannel } from '../../../../../shared/models/videos'
|
import { VideoChannel as ServerVideoChannel, viewsPerTime } 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,6 +12,7 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
|
||||||
ownerAccount?: Account
|
ownerAccount?: Account
|
||||||
ownerBy?: string
|
ownerBy?: string
|
||||||
ownerAvatarUrl?: string
|
ownerAvatarUrl?: string
|
||||||
|
viewsPerDay?: viewsPerTime[]
|
||||||
|
|
||||||
constructor (hash: ServerVideoChannel) {
|
constructor (hash: ServerVideoChannel) {
|
||||||
super(hash)
|
super(hash)
|
||||||
|
@ -23,6 +24,10 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
|
||||||
this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host)
|
this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host)
|
||||||
this.nameWithHostForced = Actor.CREATE_BY_STRING(this.name, this.host, true)
|
this.nameWithHostForced = Actor.CREATE_BY_STRING(this.name, this.host, true)
|
||||||
|
|
||||||
|
if (hash.viewsPerDay) {
|
||||||
|
this.viewsPerDay = hash.viewsPerDay.map(v => ({ ...v, date: new Date(v.date)}))
|
||||||
|
}
|
||||||
|
|
||||||
if (hash.ownerAccount) {
|
if (hash.ownerAccount) {
|
||||||
this.ownerAccount = hash.ownerAccount
|
this.ownerAccount = hash.ownerAccount
|
||||||
this.ownerBy = Actor.CREATE_BY_STRING(hash.ownerAccount.name, hash.ownerAccount.host)
|
this.ownerBy = Actor.CREATE_BY_STRING(hash.ownerAccount.name, hash.ownerAccount.host)
|
||||||
|
|
|
@ -30,7 +30,7 @@ import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttr
|
||||||
import { VideoModel } from './video'
|
import { VideoModel } from './video'
|
||||||
import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
|
import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
|
||||||
import { ServerModel } from '../server/server'
|
import { ServerModel } from '../server/server'
|
||||||
import { FindOptions, Op } from 'sequelize'
|
import { FindOptions, Op, literal, ScopeOptions } from 'sequelize'
|
||||||
import { AvatarModel } from '../avatar/avatar'
|
import { AvatarModel } from '../avatar/avatar'
|
||||||
import { VideoPlaylistModel } from './video-playlist'
|
import { VideoPlaylistModel } from './video-playlist'
|
||||||
import * as Bluebird from 'bluebird'
|
import * as Bluebird from 'bluebird'
|
||||||
|
@ -45,16 +45,21 @@ import {
|
||||||
|
|
||||||
export enum ScopeNames {
|
export enum ScopeNames {
|
||||||
FOR_API = 'FOR_API',
|
FOR_API = 'FOR_API',
|
||||||
|
SUMMARY = 'SUMMARY',
|
||||||
WITH_ACCOUNT = 'WITH_ACCOUNT',
|
WITH_ACCOUNT = 'WITH_ACCOUNT',
|
||||||
WITH_ACTOR = 'WITH_ACTOR',
|
WITH_ACTOR = 'WITH_ACTOR',
|
||||||
WITH_VIDEOS = 'WITH_VIDEOS',
|
WITH_VIDEOS = 'WITH_VIDEOS',
|
||||||
SUMMARY = 'SUMMARY'
|
WITH_STATS = 'WITH_STATS'
|
||||||
}
|
}
|
||||||
|
|
||||||
type AvailableForListOptions = {
|
type AvailableForListOptions = {
|
||||||
actorId: number
|
actorId: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AvailableWithStatsOptions = {
|
||||||
|
daysPrior: number
|
||||||
|
}
|
||||||
|
|
||||||
export type SummaryOptions = {
|
export type SummaryOptions = {
|
||||||
withAccount?: boolean // Default: false
|
withAccount?: boolean // Default: false
|
||||||
withAccountBlockerIds?: number[]
|
withAccountBlockerIds?: number[]
|
||||||
|
@ -69,40 +74,6 @@ export type SummaryOptions = {
|
||||||
]
|
]
|
||||||
}))
|
}))
|
||||||
@Scopes(() => ({
|
@Scopes(() => ({
|
||||||
[ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => {
|
|
||||||
const base: FindOptions = {
|
|
||||||
attributes: [ 'id', 'name', 'description', 'actorId' ],
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
|
|
||||||
model: ActorModel.unscoped(),
|
|
||||||
required: true,
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
attributes: [ 'host' ],
|
|
||||||
model: ServerModel.unscoped(),
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
model: AvatarModel.unscoped(),
|
|
||||||
required: false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.withAccount === true) {
|
|
||||||
base.include.push({
|
|
||||||
model: AccountModel.scope({
|
|
||||||
method: [ AccountModelScopeNames.SUMMARY, { withAccountBlockerIds: options.withAccountBlockerIds } as AccountSummaryOptions ]
|
|
||||||
}),
|
|
||||||
required: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return base
|
|
||||||
},
|
|
||||||
[ScopeNames.FOR_API]: (options: AvailableForListOptions) => {
|
[ScopeNames.FOR_API]: (options: AvailableForListOptions) => {
|
||||||
// Only list local channels OR channels that are on an instance followed by actorId
|
// Only list local channels OR channels that are on an instance followed by actorId
|
||||||
const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId)
|
const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId)
|
||||||
|
@ -143,6 +114,40 @@ export type SummaryOptions = {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
[ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => {
|
||||||
|
const base: FindOptions = {
|
||||||
|
attributes: [ 'id', 'name', 'description', 'actorId' ],
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
|
||||||
|
model: ActorModel.unscoped(),
|
||||||
|
required: true,
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
attributes: [ 'host' ],
|
||||||
|
model: ServerModel.unscoped(),
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: AvatarModel.unscoped(),
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.withAccount === true) {
|
||||||
|
base.include.push({
|
||||||
|
model: AccountModel.scope({
|
||||||
|
method: [ AccountModelScopeNames.SUMMARY, { withAccountBlockerIds: options.withAccountBlockerIds } as AccountSummaryOptions ]
|
||||||
|
}),
|
||||||
|
required: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return base
|
||||||
|
},
|
||||||
[ScopeNames.WITH_ACCOUNT]: {
|
[ScopeNames.WITH_ACCOUNT]: {
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
|
@ -151,16 +156,52 @@ export type SummaryOptions = {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
[ScopeNames.WITH_ACTOR]: {
|
||||||
|
include: [
|
||||||
|
ActorModel
|
||||||
|
]
|
||||||
|
},
|
||||||
[ScopeNames.WITH_VIDEOS]: {
|
[ScopeNames.WITH_VIDEOS]: {
|
||||||
include: [
|
include: [
|
||||||
VideoModel
|
VideoModel
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
[ScopeNames.WITH_ACTOR]: {
|
[ScopeNames.WITH_STATS]: (options: AvailableWithStatsOptions = { daysPrior: 30 }) => ({
|
||||||
include: [
|
attributes: {
|
||||||
ActorModel
|
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" ' +
|
||||||
|
'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"."createdAt") = days.day ` +
|
||||||
|
'GROUP BY 1 ' +
|
||||||
|
'ORDER BY day ' +
|
||||||
|
') t' +
|
||||||
|
')'
|
||||||
|
),
|
||||||
|
'viewsPerDay'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
}))
|
}))
|
||||||
@Table({
|
@Table({
|
||||||
tableName: 'videoChannel',
|
tableName: 'videoChannel',
|
||||||
|
@ -352,6 +393,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
|
||||||
start: number
|
start: number
|
||||||
count: number
|
count: number
|
||||||
sort: string
|
sort: string
|
||||||
|
withStats?: boolean
|
||||||
}) {
|
}) {
|
||||||
const query = {
|
const query = {
|
||||||
offset: options.start,
|
offset: options.start,
|
||||||
|
@ -368,7 +410,17 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR ]
|
||||||
|
|
||||||
|
options.withStats = true // TODO: remove beyond after initial tests
|
||||||
|
if (options.withStats) {
|
||||||
|
scopes.push({
|
||||||
|
method: [ ScopeNames.WITH_STATS, { daysPrior: 30 } as AvailableWithStatsOptions ]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return VideoChannelModel
|
return VideoChannelModel
|
||||||
|
.scope(scopes)
|
||||||
.findAndCountAll(query)
|
.findAndCountAll(query)
|
||||||
.then(({ rows, count }) => {
|
.then(({ rows, count }) => {
|
||||||
return { total: count, data: rows }
|
return { total: count, data: rows }
|
||||||
|
@ -496,6 +548,8 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
toFormattedJSON (this: MChannelFormattable): VideoChannel {
|
toFormattedJSON (this: MChannelFormattable): VideoChannel {
|
||||||
|
const viewsPerDay = this.get('viewsPerDay') as string
|
||||||
|
|
||||||
const actor = this.Actor.toFormattedJSON()
|
const actor = this.Actor.toFormattedJSON()
|
||||||
const videoChannel = {
|
const videoChannel = {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
@ -505,7 +559,16 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
|
||||||
isLocal: this.Actor.isOwned(),
|
isLocal: this.Actor.isOwned(),
|
||||||
createdAt: this.createdAt,
|
createdAt: this.createdAt,
|
||||||
updatedAt: this.updatedAt,
|
updatedAt: this.updatedAt,
|
||||||
ownerAccount: undefined
|
ownerAccount: undefined,
|
||||||
|
viewsPerDay: viewsPerDay !== undefined
|
||||||
|
? viewsPerDay.split(',').map(v => {
|
||||||
|
const o = v.split('|')
|
||||||
|
return {
|
||||||
|
date: new Date(o[0]),
|
||||||
|
views: +o[1]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON()
|
if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON()
|
||||||
|
|
|
@ -2,12 +2,18 @@ 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 = {
|
||||||
|
date: Date
|
||||||
|
views: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface VideoChannel extends Actor {
|
export interface VideoChannel extends Actor {
|
||||||
displayName: string
|
displayName: string
|
||||||
description: string
|
description: string
|
||||||
support: string
|
support: string
|
||||||
isLocal: boolean
|
isLocal: boolean
|
||||||
ownerAccount?: Account
|
ownerAccount?: Account
|
||||||
|
viewsPerDay?: viewsPerTime[] // chronologically ordered
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VideoChannelSummary {
|
export interface VideoChannelSummary {
|
||||||
|
|
Loading…
Reference in New Issue