aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/video/video-channel.ts
diff options
context:
space:
mode:
Diffstat (limited to 'server/models/video/video-channel.ts')
-rw-r--r--server/models/video/video-channel.ts176
1 files changed, 118 insertions, 58 deletions
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index e10adcb3a..642e129ff 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -30,7 +30,7 @@ import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttr
30import { VideoModel } from './video' 30import { VideoModel } from './video'
31import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' 31import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
32import { ServerModel } from '../server/server' 32import { ServerModel } from '../server/server'
33import { FindOptions, ModelIndexesOptions, Op } from 'sequelize' 33import { FindOptions, Op, literal, ScopeOptions } from 'sequelize'
34import { AvatarModel } from '../avatar/avatar' 34import { AvatarModel } from '../avatar/avatar'
35import { VideoPlaylistModel } from './video-playlist' 35import { VideoPlaylistModel } from './video-playlist'
36import * as Bluebird from 'bluebird' 36import * as Bluebird from 'bluebird'
@@ -43,30 +43,23 @@ import {
43 MChannelSummaryFormattable 43 MChannelSummaryFormattable
44} from '../../typings/models/video' 44} from '../../typings/models/video'
45 45
46// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
47const indexes: ModelIndexesOptions[] = [
48 buildTrigramSearchIndex('video_channel_name_trigram', 'name'),
49
50 {
51 fields: [ 'accountId' ]
52 },
53 {
54 fields: [ 'actorId' ]
55 }
56]
57
58export enum ScopeNames { 46export enum ScopeNames {
59 FOR_API = 'FOR_API', 47 FOR_API = 'FOR_API',
48 SUMMARY = 'SUMMARY',
60 WITH_ACCOUNT = 'WITH_ACCOUNT', 49 WITH_ACCOUNT = 'WITH_ACCOUNT',
61 WITH_ACTOR = 'WITH_ACTOR', 50 WITH_ACTOR = 'WITH_ACTOR',
62 WITH_VIDEOS = 'WITH_VIDEOS', 51 WITH_VIDEOS = 'WITH_VIDEOS',
63 SUMMARY = 'SUMMARY' 52 WITH_STATS = 'WITH_STATS'
64} 53}
65 54
66type AvailableForListOptions = { 55type AvailableForListOptions = {
67 actorId: number 56 actorId: number
68} 57}
69 58
59type AvailableWithStatsOptions = {
60 daysPrior: number
61}
62
70export type SummaryOptions = { 63export type SummaryOptions = {
71 withAccount?: boolean // Default: false 64 withAccount?: boolean // Default: false
72 withAccountBlockerIds?: number[] 65 withAccountBlockerIds?: number[]
@@ -81,40 +74,6 @@ export type SummaryOptions = {
81 ] 74 ]
82})) 75}))
83@Scopes(() => ({ 76@Scopes(() => ({
84 [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => {
85 const base: FindOptions = {
86 attributes: [ 'id', 'name', 'description', 'actorId' ],
87 include: [
88 {
89 attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
90 model: ActorModel.unscoped(),
91 required: true,
92 include: [
93 {
94 attributes: [ 'host' ],
95 model: ServerModel.unscoped(),
96 required: false
97 },
98 {
99 model: AvatarModel.unscoped(),
100 required: false
101 }
102 ]
103 }
104 ]
105 }
106
107 if (options.withAccount === true) {
108 base.include.push({
109 model: AccountModel.scope({
110 method: [ AccountModelScopeNames.SUMMARY, { withAccountBlockerIds: options.withAccountBlockerIds } as AccountSummaryOptions ]
111 }),
112 required: true
113 })
114 }
115
116 return base
117 },
118 [ScopeNames.FOR_API]: (options: AvailableForListOptions) => { 77 [ScopeNames.FOR_API]: (options: AvailableForListOptions) => {
119 // 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
120 const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId) 79 const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId)
@@ -133,7 +92,7 @@ export type SummaryOptions = {
133 }, 92 },
134 { 93 {
135 serverId: { 94 serverId: {
136 [ Op.in ]: Sequelize.literal(inQueryInstanceFollow) 95 [Op.in]: Sequelize.literal(inQueryInstanceFollow)
137 } 96 }
138 } 97 }
139 ] 98 ]
@@ -155,6 +114,40 @@ export type SummaryOptions = {
155 ] 114 ]
156 } 115 }
157 }, 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 },
158 [ScopeNames.WITH_ACCOUNT]: { 151 [ScopeNames.WITH_ACCOUNT]: {
159 include: [ 152 include: [
160 { 153 {
@@ -163,20 +156,66 @@ export type SummaryOptions = {
163 } 156 }
164 ] 157 ]
165 }, 158 },
166 [ScopeNames.WITH_VIDEOS]: { 159 [ScopeNames.WITH_ACTOR]: {
167 include: [ 160 include: [
168 VideoModel 161 ActorModel
169 ] 162 ]
170 }, 163 },
171 [ScopeNames.WITH_ACTOR]: { 164 [ScopeNames.WITH_VIDEOS]: {
172 include: [ 165 include: [
173 ActorModel 166 VideoModel
174 ] 167 ]
168 },
169 [ScopeNames.WITH_STATS]: (options: AvailableWithStatsOptions = { daysPrior: 30 }) => {
170 const daysPrior = parseInt(options.daysPrior + '', 10)
171
172 return {
173 attributes: {
174 include: [
175 [
176 literal(
177 '(' +
178 `SELECT string_agg(concat_ws('|', t.day, t.views), ',') ` +
179 'FROM ( ' +
180 'WITH ' +
181 'days AS ( ' +
182 `SELECT generate_series(date_trunc('day', now()) - '${daysPrior} day'::interval, ` +
183 `date_trunc('day', now()), '1 day'::interval) AS day ` +
184 '), ' +
185 'views AS ( ' +
186 'SELECT v.* ' +
187 'FROM "videoView" AS v ' +
188 'INNER JOIN "video" ON "video"."id" = v."videoId" ' +
189 'WHERE "video"."channelId" = "VideoChannelModel"."id" ' +
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"."startDate") = date_trunc('day', days.day) ` +
195 'GROUP BY day ' +
196 'ORDER BY day ' +
197 ') t' +
198 ')'
199 ),
200 'viewsPerDay'
201 ]
202 ]
203 }
204 }
175 } 205 }
176})) 206}))
177@Table({ 207@Table({
178 tableName: 'videoChannel', 208 tableName: 'videoChannel',
179 indexes 209 indexes: [
210 buildTrigramSearchIndex('video_channel_name_trigram', 'name'),
211
212 {
213 fields: [ 'accountId' ]
214 },
215 {
216 fields: [ 'actorId' ]
217 }
218 ]
180}) 219})
181export class VideoChannelModel extends Model<VideoChannelModel> { 220export class VideoChannelModel extends Model<VideoChannelModel> {
182 221
@@ -351,10 +390,11 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
351 } 390 }
352 391
353 static listByAccount (options: { 392 static listByAccount (options: {
354 accountId: number, 393 accountId: number
355 start: number, 394 start: number
356 count: number, 395 count: number
357 sort: string 396 sort: string
397 withStats?: boolean
358 }) { 398 }) {
359 const query = { 399 const query = {
360 offset: options.start, 400 offset: options.start,
@@ -371,7 +411,16 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
371 ] 411 ]
372 } 412 }
373 413
414 const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR ]
415
416 if (options.withStats) {
417 scopes.push({
418 method: [ ScopeNames.WITH_STATS, { daysPrior: 30 } as AvailableWithStatsOptions ]
419 })
420 }
421
374 return VideoChannelModel 422 return VideoChannelModel
423 .scope(scopes)
375 .findAndCountAll(query) 424 .findAndCountAll(query)
376 .then(({ rows, count }) => { 425 .then(({ rows, count }) => {
377 return { total: count, data: rows } 426 return { total: count, data: rows }
@@ -499,6 +548,8 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
499 } 548 }
500 549
501 toFormattedJSON (this: MChannelFormattable): VideoChannel { 550 toFormattedJSON (this: MChannelFormattable): VideoChannel {
551 const viewsPerDay = this.get('viewsPerDay') as string
552
502 const actor = this.Actor.toFormattedJSON() 553 const actor = this.Actor.toFormattedJSON()
503 const videoChannel = { 554 const videoChannel = {
504 id: this.id, 555 id: this.id,
@@ -508,7 +559,16 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
508 isLocal: this.Actor.isOwned(), 559 isLocal: this.Actor.isOwned(),
509 createdAt: this.createdAt, 560 createdAt: this.createdAt,
510 updatedAt: this.updatedAt, 561 updatedAt: this.updatedAt,
511 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
512 } 572 }
513 573
514 if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON() 574 if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON()