import { ConfirmService } from '../../core/confirm'
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
+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'
@Component({
selector: 'my-account-video-channels',
})
export class MyAccountVideoChannelsComponent implements OnInit {
videoChannels: VideoChannel[] = []
+ videoChannelsData: any[]
+ videoChannelsMinimumDailyViews = 0
+ videoChannelsMaximumDailyViews: number
private user: User
private notifier: Notifier,
private confirmService: ConfirmService,
private videoChannelService: VideoChannelService,
+ private screenService: ScreenService,
private i18n: I18n
) {}
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) {
const res = await this.confirmService.confirmWithInput(
this.i18n(
private loadVideoChannels () {
this.authService.userInformationLoaded
.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
+ })
}
}
import { VideoModel } from './video'
import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
import { ServerModel } from '../server/server'
-import { FindOptions, Op } from 'sequelize'
+import { FindOptions, Op, literal, ScopeOptions } from 'sequelize'
import { AvatarModel } from '../avatar/avatar'
import { VideoPlaylistModel } from './video-playlist'
import * as Bluebird from 'bluebird'
export enum ScopeNames {
FOR_API = 'FOR_API',
+ SUMMARY = 'SUMMARY',
WITH_ACCOUNT = 'WITH_ACCOUNT',
WITH_ACTOR = 'WITH_ACTOR',
WITH_VIDEOS = 'WITH_VIDEOS',
- SUMMARY = 'SUMMARY'
+ WITH_STATS = 'WITH_STATS'
}
type AvailableForListOptions = {
actorId: number
}
+type AvailableWithStatsOptions = {
+ daysPrior: number
+}
+
export type SummaryOptions = {
withAccount?: boolean // Default: false
withAccountBlockerIds?: number[]
]
}))
@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) => {
// Only list local channels OR channels that are on an instance followed by actorId
const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId)
]
}
},
+ [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]: {
include: [
{
}
]
},
- [ScopeNames.WITH_VIDEOS]: {
+ [ScopeNames.WITH_ACTOR]: {
include: [
- VideoModel
+ ActorModel
]
},
- [ScopeNames.WITH_ACTOR]: {
+ [ScopeNames.WITH_VIDEOS]: {
include: [
- ActorModel
+ 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" ' +
+ '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({
tableName: 'videoChannel',
start: number
count: number
sort: string
+ withStats?: boolean
}) {
const query = {
offset: options.start,
]
}
+ 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
+ .scope(scopes)
.findAndCountAll(query)
.then(({ rows, count }) => {
return { total: count, data: rows }
}
toFormattedJSON (this: MChannelFormattable): VideoChannel {
+ const viewsPerDay = this.get('viewsPerDay') as string
+
const actor = this.Actor.toFormattedJSON()
const videoChannel = {
id: this.id,
isLocal: this.Actor.isOwned(),
createdAt: this.createdAt,
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()