diff options
Diffstat (limited to 'server/models/video/video-channel.ts')
-rw-r--r-- | server/models/video/video-channel.ts | 176 |
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 | |||
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, ModelIndexesOptions, 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' |
@@ -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 | ||
47 | const indexes: ModelIndexesOptions[] = [ | ||
48 | buildTrigramSearchIndex('video_channel_name_trigram', 'name'), | ||
49 | |||
50 | { | ||
51 | fields: [ 'accountId' ] | ||
52 | }, | ||
53 | { | ||
54 | fields: [ 'actorId' ] | ||
55 | } | ||
56 | ] | ||
57 | |||
58 | export enum ScopeNames { | 46 | export 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 | ||
66 | type AvailableForListOptions = { | 55 | type AvailableForListOptions = { |
67 | actorId: number | 56 | actorId: number |
68 | } | 57 | } |
69 | 58 | ||
59 | type AvailableWithStatsOptions = { | ||
60 | daysPrior: number | ||
61 | } | ||
62 | |||
70 | export type SummaryOptions = { | 63 | export 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 | }) |
181 | export class VideoChannelModel extends Model<VideoChannelModel> { | 220 | export 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() |