]>
Commit | Line | Data |
---|---|---|
fa47956e | 1 | import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions, Transaction, WhereOptions } from 'sequelize' |
3fd3ab2d | 2 | import { |
06a05d5f C |
3 | AllowNull, |
4 | BeforeDestroy, | |
5 | BelongsTo, | |
6 | Column, | |
7 | CreatedAt, | |
8 | DataType, | |
9 | Default, | |
10 | DefaultScope, | |
11 | ForeignKey, | |
6dd9de95 | 12 | HasMany, |
06a05d5f C |
13 | Is, |
14 | Model, | |
15 | Scopes, | |
f37dc0dd | 16 | Sequelize, |
06a05d5f C |
17 | Table, |
18 | UpdatedAt | |
3fd3ab2d | 19 | } from 'sequelize-typescript' |
a59f210f | 20 | import { MAccountActor } from '@server/types/models' |
b033851f | 21 | import { AttributesOnly, pick } from '@shared/core-utils' |
50d6de9c | 22 | import { ActivityPubActor } from '../../../shared/models/activitypub' |
418d092a | 23 | import { VideoChannel, VideoChannelSummary } from '../../../shared/models/videos' |
2422c46b | 24 | import { |
06a05d5f | 25 | isVideoChannelDescriptionValid, |
27db7840 | 26 | isVideoChannelDisplayNameValid, |
2422c46b C |
27 | isVideoChannelSupportValid |
28 | } from '../../helpers/custom-validators/video-channels' | |
754b6f5f | 29 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' |
2af337c8 | 30 | import { sendDeleteActor } from '../../lib/activitypub/send' |
453e83ea | 31 | import { |
453e83ea | 32 | MChannelActor, |
b5fecbf4 | 33 | MChannelAP, |
2cb03dc1 | 34 | MChannelBannerAccountDefault, |
b5fecbf4 C |
35 | MChannelFormattable, |
36 | MChannelSummaryFormattable | |
26d6bf65 | 37 | } from '../../types/models/video' |
2af337c8 | 38 | import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' |
7d9ba5c0 C |
39 | import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor' |
40 | import { ActorFollowModel } from '../actor/actor-follow' | |
41 | import { ActorImageModel } from '../actor/actor-image' | |
2af337c8 | 42 | import { ServerModel } from '../server/server' |
fa47956e | 43 | import { setAsUpdated } from '../shared' |
2af337c8 C |
44 | import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' |
45 | import { VideoModel } from './video' | |
46 | import { VideoPlaylistModel } from './video-playlist' | |
754b6f5f | 47 | import { CONFIG } from '@server/initializers/config' |
f37dc0dd | 48 | |
418d092a | 49 | export enum ScopeNames { |
453e83ea | 50 | FOR_API = 'FOR_API', |
8165d00a | 51 | SUMMARY = 'SUMMARY', |
d48ff09d | 52 | WITH_ACCOUNT = 'WITH_ACCOUNT', |
50d6de9c | 53 | WITH_ACTOR = 'WITH_ACTOR', |
2cb03dc1 | 54 | WITH_ACTOR_BANNER = 'WITH_ACTOR_BANNER', |
418d092a | 55 | WITH_VIDEOS = 'WITH_VIDEOS', |
8165d00a | 56 | WITH_STATS = 'WITH_STATS' |
d48ff09d C |
57 | } |
58 | ||
f37dc0dd C |
59 | type AvailableForListOptions = { |
60 | actorId: number | |
bc99dfe5 | 61 | search?: string |
fa47956e | 62 | host?: string |
b033851f | 63 | handles?: string[] |
f37dc0dd C |
64 | } |
65 | ||
8165d00a RK |
66 | type AvailableWithStatsOptions = { |
67 | daysPrior: number | |
68 | } | |
69 | ||
bfbd9128 | 70 | export type SummaryOptions = { |
4f32032f | 71 | actorRequired?: boolean // Default: true |
bfbd9128 C |
72 | withAccount?: boolean // Default: false |
73 | withAccountBlockerIds?: number[] | |
74 | } | |
75 | ||
3acc5084 | 76 | @DefaultScope(() => ({ |
50d6de9c C |
77 | include: [ |
78 | { | |
3acc5084 | 79 | model: ActorModel, |
50d6de9c C |
80 | required: true |
81 | } | |
82 | ] | |
3acc5084 C |
83 | })) |
84 | @Scopes(() => ({ | |
453e83ea | 85 | [ScopeNames.FOR_API]: (options: AvailableForListOptions) => { |
f37dc0dd | 86 | // Only list local channels OR channels that are on an instance followed by actorId |
418d092a | 87 | const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId) |
f37dc0dd | 88 | |
fbd67e7f C |
89 | const whereActorAnd: WhereOptions[] = [ |
90 | { | |
91 | [Op.or]: [ | |
92 | { | |
93 | serverId: null | |
94 | }, | |
95 | { | |
96 | serverId: { | |
97 | [Op.in]: Sequelize.literal(inQueryInstanceFollow) | |
98 | } | |
fa47956e | 99 | } |
fbd67e7f C |
100 | ] |
101 | } | |
102 | ] | |
fa47956e C |
103 | |
104 | let serverRequired = false | |
105 | let whereServer: WhereOptions | |
106 | ||
107 | if (options.host && options.host !== WEBSERVER.HOST) { | |
108 | serverRequired = true | |
109 | whereServer = { host: options.host } | |
110 | } | |
111 | ||
112 | if (options.host === WEBSERVER.HOST) { | |
fbd67e7f C |
113 | whereActorAnd.push({ |
114 | serverId: null | |
115 | }) | |
116 | } | |
117 | ||
b033851f C |
118 | let rootWhere: WhereOptions |
119 | if (options.handles) { | |
120 | const or: WhereOptions[] = [] | |
121 | ||
122 | for (const handle of options.handles || []) { | |
123 | const [ preferredUsername, host ] = handle.split('@') | |
124 | ||
93a1e67f | 125 | if (!host || host === WEBSERVER.HOST) { |
b033851f C |
126 | or.push({ |
127 | '$Actor.preferredUsername$': preferredUsername, | |
128 | '$Actor.serverId$': null | |
129 | }) | |
130 | } else { | |
131 | or.push({ | |
132 | '$Actor.preferredUsername$': preferredUsername, | |
133 | '$Actor.Server.host$': host | |
134 | }) | |
fbd67e7f | 135 | } |
b033851f C |
136 | } |
137 | ||
138 | rootWhere = { | |
139 | [Op.or]: or | |
140 | } | |
fa47956e C |
141 | } |
142 | ||
f37dc0dd | 143 | return { |
b033851f | 144 | where: rootWhere, |
f37dc0dd C |
145 | include: [ |
146 | { | |
147 | attributes: { | |
148 | exclude: unusedActorAttributesForAPI | |
149 | }, | |
150 | model: ActorModel, | |
fbd67e7f C |
151 | where: { |
152 | [Op.and]: whereActorAnd | |
153 | }, | |
213e30ef | 154 | include: [ |
fa47956e C |
155 | { |
156 | model: ServerModel, | |
157 | required: serverRequired, | |
158 | where: whereServer | |
159 | }, | |
160 | { | |
161 | model: ActorImageModel, | |
162 | as: 'Avatar', | |
163 | required: false | |
164 | }, | |
213e30ef C |
165 | { |
166 | model: ActorImageModel, | |
167 | as: 'Banner', | |
168 | required: false | |
169 | } | |
170 | ] | |
f37dc0dd C |
171 | }, |
172 | { | |
173 | model: AccountModel, | |
174 | required: true, | |
175 | include: [ | |
176 | { | |
177 | attributes: { | |
178 | exclude: unusedActorAttributesForAPI | |
179 | }, | |
180 | model: ActorModel, // Default scope includes avatar and server | |
181 | required: true | |
182 | } | |
183 | ] | |
184 | } | |
185 | ] | |
186 | } | |
187 | }, | |
8165d00a | 188 | [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => { |
b49f22d8 C |
189 | const include: Includeable[] = [ |
190 | { | |
191 | attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], | |
192 | model: ActorModel.unscoped(), | |
193 | required: options.actorRequired ?? true, | |
194 | include: [ | |
195 | { | |
196 | attributes: [ 'host' ], | |
197 | model: ServerModel.unscoped(), | |
198 | required: false | |
199 | }, | |
200 | { | |
f4796856 C |
201 | model: ActorImageModel.unscoped(), |
202 | as: 'Avatar', | |
b49f22d8 C |
203 | required: false |
204 | } | |
205 | ] | |
206 | } | |
207 | ] | |
208 | ||
8165d00a | 209 | const base: FindOptions = { |
b49f22d8 | 210 | attributes: [ 'id', 'name', 'description', 'actorId' ] |
8165d00a RK |
211 | } |
212 | ||
213 | if (options.withAccount === true) { | |
b49f22d8 | 214 | include.push({ |
8165d00a RK |
215 | model: AccountModel.scope({ |
216 | method: [ AccountModelScopeNames.SUMMARY, { withAccountBlockerIds: options.withAccountBlockerIds } as AccountSummaryOptions ] | |
217 | }), | |
218 | required: true | |
219 | }) | |
220 | } | |
221 | ||
b49f22d8 C |
222 | base.include = include |
223 | ||
8165d00a RK |
224 | return base |
225 | }, | |
f37dc0dd C |
226 | [ScopeNames.WITH_ACCOUNT]: { |
227 | include: [ | |
228 | { | |
3acc5084 | 229 | model: AccountModel, |
f37dc0dd | 230 | required: true |
d48ff09d C |
231 | } |
232 | ] | |
233 | }, | |
8165d00a | 234 | [ScopeNames.WITH_ACTOR]: { |
d48ff09d | 235 | include: [ |
8165d00a | 236 | ActorModel |
d48ff09d | 237 | ] |
50d6de9c | 238 | }, |
2cb03dc1 C |
239 | [ScopeNames.WITH_ACTOR_BANNER]: { |
240 | include: [ | |
241 | { | |
242 | model: ActorModel, | |
243 | include: [ | |
244 | { | |
245 | model: ActorImageModel, | |
246 | required: false, | |
247 | as: 'Banner' | |
248 | } | |
249 | ] | |
250 | } | |
251 | ] | |
252 | }, | |
8165d00a | 253 | [ScopeNames.WITH_VIDEOS]: { |
50d6de9c | 254 | include: [ |
8165d00a | 255 | VideoModel |
50d6de9c | 256 | ] |
8165d00a | 257 | }, |
3d527ba1 RK |
258 | [ScopeNames.WITH_STATS]: (options: AvailableWithStatsOptions = { daysPrior: 30 }) => { |
259 | const daysPrior = parseInt(options.daysPrior + '', 10) | |
260 | ||
261 | return { | |
262 | attributes: { | |
263 | include: [ | |
1ba471c5 C |
264 | [ |
265 | literal('(SELECT COUNT(*) FROM "video" WHERE "channelId" = "VideoChannelModel"."id")'), | |
266 | 'videosCount' | |
267 | ], | |
3d527ba1 RK |
268 | [ |
269 | literal( | |
270 | '(' + | |
271 | `SELECT string_agg(concat_ws('|', t.day, t.views), ',') ` + | |
272 | 'FROM ( ' + | |
273 | 'WITH ' + | |
274 | 'days AS ( ' + | |
275 | `SELECT generate_series(date_trunc('day', now()) - '${daysPrior} day'::interval, ` + | |
276 | `date_trunc('day', now()), '1 day'::interval) AS day ` + | |
8165d00a | 277 | ') ' + |
5a61ffbb C |
278 | 'SELECT days.day AS day, COALESCE(SUM("videoView".views), 0) AS views ' + |
279 | 'FROM days ' + | |
280 | 'LEFT JOIN (' + | |
281 | '"videoView" INNER JOIN "video" ON "videoView"."videoId" = "video"."id" ' + | |
282 | 'AND "video"."channelId" = "VideoChannelModel"."id"' + | |
283 | `) ON date_trunc('day', "videoView"."startDate") = date_trunc('day', days.day) ` + | |
284 | 'GROUP BY day ' + | |
285 | 'ORDER BY day ' + | |
286 | ') t' + | |
3d527ba1 RK |
287 | ')' |
288 | ), | |
289 | 'viewsPerDay' | |
290 | ] | |
8165d00a | 291 | ] |
3d527ba1 | 292 | } |
8165d00a | 293 | } |
3d527ba1 | 294 | } |
3acc5084 | 295 | })) |
3fd3ab2d C |
296 | @Table({ |
297 | tableName: 'videoChannel', | |
0374b6b5 C |
298 | indexes: [ |
299 | buildTrigramSearchIndex('video_channel_name_trigram', 'name'), | |
300 | ||
301 | { | |
302 | fields: [ 'accountId' ] | |
303 | }, | |
304 | { | |
305 | fields: [ 'actorId' ] | |
306 | } | |
307 | ] | |
3fd3ab2d | 308 | }) |
16c016e8 | 309 | export class VideoChannelModel extends Model<Partial<AttributesOnly<VideoChannelModel>>> { |
72c7248b | 310 | |
3fd3ab2d | 311 | @AllowNull(false) |
27db7840 | 312 | @Is('VideoChannelName', value => throwIfNotValid(value, isVideoChannelDisplayNameValid, 'name')) |
3fd3ab2d C |
313 | @Column |
314 | name: string | |
72c7248b | 315 | |
3fd3ab2d | 316 | @AllowNull(true) |
2422c46b | 317 | @Default(null) |
1735c825 | 318 | @Is('VideoChannelDescription', value => throwIfNotValid(value, isVideoChannelDescriptionValid, 'description', true)) |
a10fc78b | 319 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.DESCRIPTION.max)) |
3fd3ab2d | 320 | description: string |
72c7248b | 321 | |
2422c46b C |
322 | @AllowNull(true) |
323 | @Default(null) | |
1735c825 | 324 | @Is('VideoChannelSupport', value => throwIfNotValid(value, isVideoChannelSupportValid, 'support', true)) |
a10fc78b | 325 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.SUPPORT.max)) |
2422c46b C |
326 | support: string |
327 | ||
3fd3ab2d C |
328 | @CreatedAt |
329 | createdAt: Date | |
72c7248b | 330 | |
3fd3ab2d C |
331 | @UpdatedAt |
332 | updatedAt: Date | |
4e50b6a1 | 333 | |
fadf619a C |
334 | @ForeignKey(() => ActorModel) |
335 | @Column | |
336 | actorId: number | |
337 | ||
338 | @BelongsTo(() => ActorModel, { | |
339 | foreignKey: { | |
340 | allowNull: false | |
341 | }, | |
342 | onDelete: 'cascade' | |
343 | }) | |
344 | Actor: ActorModel | |
345 | ||
3fd3ab2d C |
346 | @ForeignKey(() => AccountModel) |
347 | @Column | |
348 | accountId: number | |
4e50b6a1 | 349 | |
3fd3ab2d C |
350 | @BelongsTo(() => AccountModel, { |
351 | foreignKey: { | |
352 | allowNull: false | |
06c27593 | 353 | } |
3fd3ab2d C |
354 | }) |
355 | Account: AccountModel | |
72c7248b | 356 | |
3fd3ab2d | 357 | @HasMany(() => VideoModel, { |
72c7248b | 358 | foreignKey: { |
3fd3ab2d | 359 | name: 'channelId', |
72c7248b C |
360 | allowNull: false |
361 | }, | |
f05a1c30 C |
362 | onDelete: 'CASCADE', |
363 | hooks: true | |
72c7248b | 364 | }) |
3fd3ab2d | 365 | Videos: VideoModel[] |
72c7248b | 366 | |
418d092a C |
367 | @HasMany(() => VideoPlaylistModel, { |
368 | foreignKey: { | |
07b1a18a | 369 | allowNull: true |
418d092a | 370 | }, |
df0b219d | 371 | onDelete: 'CASCADE', |
418d092a C |
372 | hooks: true |
373 | }) | |
374 | VideoPlaylists: VideoPlaylistModel[] | |
375 | ||
f05a1c30 C |
376 | @BeforeDestroy |
377 | static async sendDeleteIfOwned (instance: VideoChannelModel, options) { | |
378 | if (!instance.Actor) { | |
e6122097 | 379 | instance.Actor = await instance.$get('Actor', { transaction: options.transaction }) |
f05a1c30 C |
380 | } |
381 | ||
2af337c8 C |
382 | await ActorFollowModel.removeFollowsOf(instance.Actor.id, options.transaction) |
383 | ||
c5a893d5 | 384 | if (instance.Actor.isOwned()) { |
c5a893d5 C |
385 | return sendDeleteActor(instance.Actor, options.transaction) |
386 | } | |
387 | ||
388 | return undefined | |
3fd3ab2d | 389 | } |
72c7248b | 390 | |
3fd3ab2d C |
391 | static countByAccount (accountId: number) { |
392 | const query = { | |
393 | where: { | |
394 | accountId | |
395 | } | |
72c7248b | 396 | } |
3fd3ab2d C |
397 | |
398 | return VideoChannelModel.count(query) | |
72c7248b C |
399 | } |
400 | ||
fe19f600 RK |
401 | static async getStats () { |
402 | ||
403 | function getActiveVideoChannels (days: number) { | |
404 | const options = { | |
405 | type: QueryTypes.SELECT as QueryTypes.SELECT, | |
406 | raw: true | |
407 | } | |
408 | ||
409 | const query = ` | |
410 | SELECT COUNT(DISTINCT("VideoChannelModel"."id")) AS "count" | |
411 | FROM "videoChannel" AS "VideoChannelModel" | |
412 | INNER JOIN "video" AS "Videos" | |
413 | ON "VideoChannelModel"."id" = "Videos"."channelId" | |
414 | AND ("Videos"."publishedAt" > Now() - interval '${days}d') | |
415 | INNER JOIN "account" AS "Account" | |
416 | ON "VideoChannelModel"."accountId" = "Account"."id" | |
417 | INNER JOIN "actor" AS "Account->Actor" | |
418 | ON "Account"."actorId" = "Account->Actor"."id" | |
419 | AND "Account->Actor"."serverId" IS NULL | |
420 | LEFT OUTER JOIN "server" AS "Account->Actor->Server" | |
421 | ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |
422 | ||
423 | return VideoChannelModel.sequelize.query<{ count: string }>(query, options) | |
424 | .then(r => parseInt(r[0].count, 10)) | |
425 | } | |
426 | ||
427 | const totalLocalVideoChannels = await VideoChannelModel.count() | |
428 | const totalLocalDailyActiveVideoChannels = await getActiveVideoChannels(1) | |
429 | const totalLocalWeeklyActiveVideoChannels = await getActiveVideoChannels(7) | |
430 | const totalLocalMonthlyActiveVideoChannels = await getActiveVideoChannels(30) | |
431 | const totalHalfYearActiveVideoChannels = await getActiveVideoChannels(180) | |
432 | ||
433 | return { | |
434 | totalLocalVideoChannels, | |
435 | totalLocalDailyActiveVideoChannels, | |
436 | totalLocalWeeklyActiveVideoChannels, | |
437 | totalLocalMonthlyActiveVideoChannels, | |
438 | totalHalfYearActiveVideoChannels | |
439 | } | |
440 | } | |
441 | ||
b49f22d8 | 442 | static listLocalsForSitemap (sort: string): Promise<MChannelActor[]> { |
2feebf3e C |
443 | const query = { |
444 | attributes: [ ], | |
445 | offset: 0, | |
446 | order: getSort(sort), | |
447 | include: [ | |
448 | { | |
449 | attributes: [ 'preferredUsername', 'serverId' ], | |
450 | model: ActorModel.unscoped(), | |
451 | where: { | |
452 | serverId: null | |
453 | } | |
454 | } | |
455 | ] | |
456 | } | |
457 | ||
458 | return VideoChannelModel | |
459 | .unscoped() | |
460 | .findAll(query) | |
461 | } | |
462 | ||
9c9a236b | 463 | static listForApi (parameters: Pick<AvailableForListOptions, 'actorId'> & { |
f37dc0dd C |
464 | start: number |
465 | count: number | |
466 | sort: string | |
9c9a236b C |
467 | }) { |
468 | const { actorId } = parameters | |
fa47956e | 469 | |
9c9a236b C |
470 | const query = { |
471 | offset: parameters.start, | |
472 | limit: parameters.count, | |
473 | order: getSort(parameters.sort) | |
474 | } | |
475 | ||
476 | return VideoChannelModel | |
477 | .scope({ | |
478 | method: [ ScopeNames.FOR_API, { actorId } as AvailableForListOptions ] | |
479 | }) | |
480 | .findAndCountAll(query) | |
481 | .then(({ rows, count }) => { | |
482 | return { total: count, data: rows } | |
483 | }) | |
484 | } | |
485 | ||
486 | static searchForApi (options: Pick<AvailableForListOptions, 'actorId' | 'search' | 'host' | 'handles'> & { | |
487 | start: number | |
488 | count: number | |
489 | sort: string | |
f37dc0dd | 490 | }) { |
fbd67e7f C |
491 | let attributesInclude: any[] = [ literal('0 as similarity') ] |
492 | let where: WhereOptions | |
f37dc0dd | 493 | |
fbd67e7f C |
494 | if (options.search) { |
495 | const escapedSearch = VideoChannelModel.sequelize.escape(options.search) | |
496 | const escapedLikeSearch = VideoChannelModel.sequelize.escape('%' + options.search + '%') | |
497 | attributesInclude = [ createSimilarityAttribute('VideoChannelModel.name', options.search) ] | |
498 | ||
499 | where = { | |
1735c825 | 500 | [Op.or]: [ |
c3c2ab1c C |
501 | Sequelize.literal( |
502 | 'lower(immutable_unaccent("VideoChannelModel"."name")) % lower(immutable_unaccent(' + escapedSearch + '))' | |
503 | ), | |
504 | Sequelize.literal( | |
505 | 'lower(immutable_unaccent("VideoChannelModel"."name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))' | |
f37dc0dd | 506 | ) |
c3c2ab1c | 507 | ] |
f37dc0dd C |
508 | } |
509 | } | |
510 | ||
fbd67e7f C |
511 | const query = { |
512 | attributes: { | |
513 | include: attributesInclude | |
514 | }, | |
515 | offset: options.start, | |
516 | limit: options.count, | |
517 | order: getSort(options.sort), | |
518 | where | |
519 | } | |
520 | ||
f37dc0dd | 521 | return VideoChannelModel |
b49f22d8 | 522 | .scope({ |
b033851f | 523 | method: [ ScopeNames.FOR_API, pick(options, [ 'actorId', 'host', 'handles' ]) as AvailableForListOptions ] |
b49f22d8 | 524 | }) |
50d6de9c | 525 | .findAndCountAll(query) |
3fd3ab2d C |
526 | .then(({ rows, count }) => { |
527 | return { total: count, data: rows } | |
528 | }) | |
72c7248b C |
529 | } |
530 | ||
4beda9e1 | 531 | static listByAccountForAPI (options: { |
a1587156 C |
532 | accountId: number |
533 | start: number | |
534 | count: number | |
91b66319 | 535 | sort: string |
8165d00a | 536 | withStats?: boolean |
4f5d0459 | 537 | search?: string |
91b66319 | 538 | }) { |
4f5d0459 RK |
539 | const escapedSearch = VideoModel.sequelize.escape(options.search) |
540 | const escapedLikeSearch = VideoModel.sequelize.escape('%' + options.search + '%') | |
541 | const where = options.search | |
542 | ? { | |
543 | [Op.or]: [ | |
544 | Sequelize.literal( | |
545 | 'lower(immutable_unaccent("VideoChannelModel"."name")) % lower(immutable_unaccent(' + escapedSearch + '))' | |
546 | ), | |
547 | Sequelize.literal( | |
548 | 'lower(immutable_unaccent("VideoChannelModel"."name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))' | |
549 | ) | |
550 | ] | |
551 | } | |
552 | : null | |
553 | ||
3fd3ab2d | 554 | const query = { |
91b66319 C |
555 | offset: options.start, |
556 | limit: options.count, | |
557 | order: getSort(options.sort), | |
3fd3ab2d C |
558 | include: [ |
559 | { | |
560 | model: AccountModel, | |
561 | where: { | |
91b66319 | 562 | id: options.accountId |
3fd3ab2d | 563 | }, |
50d6de9c | 564 | required: true |
3fd3ab2d | 565 | } |
4f5d0459 RK |
566 | ], |
567 | where | |
3fd3ab2d | 568 | } |
72c7248b | 569 | |
2cb03dc1 | 570 | const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR_BANNER ] |
8165d00a | 571 | |
5a61ffbb | 572 | if (options.withStats === true) { |
8165d00a RK |
573 | scopes.push({ |
574 | method: [ ScopeNames.WITH_STATS, { daysPrior: 30 } as AvailableWithStatsOptions ] | |
575 | }) | |
576 | } | |
577 | ||
50d6de9c | 578 | return VideoChannelModel |
8165d00a | 579 | .scope(scopes) |
50d6de9c | 580 | .findAndCountAll(query) |
3fd3ab2d C |
581 | .then(({ rows, count }) => { |
582 | return { total: count, data: rows } | |
583 | }) | |
72c7248b C |
584 | } |
585 | ||
4beda9e1 C |
586 | static listAllByAccount (accountId: number) { |
587 | const query = { | |
754b6f5f | 588 | limit: CONFIG.VIDEO_CHANNELS.MAX_PER_USER, |
4beda9e1 C |
589 | include: [ |
590 | { | |
591 | attributes: [], | |
592 | model: AccountModel, | |
593 | where: { | |
594 | id: accountId | |
595 | }, | |
596 | required: true | |
597 | } | |
598 | ] | |
599 | } | |
600 | ||
601 | return VideoChannelModel.findAll(query) | |
602 | } | |
603 | ||
eae0365b | 604 | static loadAndPopulateAccount (id: number, transaction?: Transaction): Promise<MChannelBannerAccountDefault> { |
5cf84858 | 605 | return VideoChannelModel.unscoped() |
2cb03dc1 | 606 | .scope([ ScopeNames.WITH_ACTOR_BANNER, ScopeNames.WITH_ACCOUNT ]) |
eae0365b | 607 | .findByPk(id, { transaction }) |
3fd3ab2d | 608 | } |
0d0e8dd0 | 609 | |
2cb03dc1 | 610 | static loadByUrlAndPopulateAccount (url: string): Promise<MChannelBannerAccountDefault> { |
f37dc0dd C |
611 | const query = { |
612 | include: [ | |
613 | { | |
614 | model: ActorModel, | |
615 | required: true, | |
616 | where: { | |
617 | url | |
2cb03dc1 C |
618 | }, |
619 | include: [ | |
620 | { | |
621 | model: ActorImageModel, | |
622 | required: false, | |
623 | as: 'Banner' | |
624 | } | |
625 | ] | |
f37dc0dd C |
626 | } |
627 | ] | |
628 | } | |
629 | ||
630 | return VideoChannelModel | |
631 | .scope([ ScopeNames.WITH_ACCOUNT ]) | |
8a19bee1 | 632 | .findOne(query) |
72c7248b C |
633 | } |
634 | ||
92bf2f62 C |
635 | static loadByNameWithHostAndPopulateAccount (nameWithHost: string) { |
636 | const [ name, host ] = nameWithHost.split('@') | |
637 | ||
6dd9de95 | 638 | if (!host || host === WEBSERVER.HOST) return VideoChannelModel.loadLocalByNameAndPopulateAccount(name) |
92bf2f62 C |
639 | |
640 | return VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host) | |
641 | } | |
642 | ||
2cb03dc1 | 643 | static loadLocalByNameAndPopulateAccount (name: string): Promise<MChannelBannerAccountDefault> { |
8a19bee1 | 644 | const query = { |
3fd3ab2d | 645 | include: [ |
8a19bee1 C |
646 | { |
647 | model: ActorModel, | |
648 | required: true, | |
649 | where: { | |
650 | preferredUsername: name, | |
651 | serverId: null | |
2cb03dc1 C |
652 | }, |
653 | include: [ | |
654 | { | |
655 | model: ActorImageModel, | |
656 | required: false, | |
657 | as: 'Banner' | |
658 | } | |
659 | ] | |
8a19bee1 | 660 | } |
3fd3ab2d C |
661 | ] |
662 | } | |
72c7248b | 663 | |
5cf84858 | 664 | return VideoChannelModel.unscoped() |
2cb03dc1 | 665 | .scope([ ScopeNames.WITH_ACCOUNT ]) |
8a19bee1 | 666 | .findOne(query) |
72c7248b C |
667 | } |
668 | ||
2cb03dc1 | 669 | static loadByNameAndHostAndPopulateAccount (name: string, host: string): Promise<MChannelBannerAccountDefault> { |
06a05d5f C |
670 | const query = { |
671 | include: [ | |
672 | { | |
673 | model: ActorModel, | |
674 | required: true, | |
675 | where: { | |
8a19bee1 C |
676 | preferredUsername: name |
677 | }, | |
678 | include: [ | |
679 | { | |
680 | model: ServerModel, | |
681 | required: true, | |
682 | where: { host } | |
2cb03dc1 C |
683 | }, |
684 | { | |
685 | model: ActorImageModel, | |
686 | required: false, | |
687 | as: 'Banner' | |
8a19bee1 C |
688 | } |
689 | ] | |
06a05d5f C |
690 | } |
691 | ] | |
692 | } | |
693 | ||
5cf84858 | 694 | return VideoChannelModel.unscoped() |
2cb03dc1 | 695 | .scope([ ScopeNames.WITH_ACCOUNT ]) |
8a19bee1 C |
696 | .findOne(query) |
697 | } | |
698 | ||
1ca9f7c3 C |
699 | toFormattedSummaryJSON (this: MChannelSummaryFormattable): VideoChannelSummary { |
700 | const actor = this.Actor.toFormattedSummaryJSON() | |
701 | ||
702 | return { | |
703 | id: this.id, | |
704 | name: actor.name, | |
705 | displayName: this.getDisplayName(), | |
706 | url: actor.url, | |
707 | host: actor.host, | |
708 | avatar: actor.avatar | |
709 | } | |
710 | } | |
711 | ||
712 | toFormattedJSON (this: MChannelFormattable): VideoChannel { | |
1ba471c5 C |
713 | const viewsPerDayString = this.get('viewsPerDay') as string |
714 | const videosCount = this.get('videosCount') as number | |
715 | ||
716 | let viewsPerDay: { date: Date, views: number }[] | |
717 | ||
718 | if (viewsPerDayString) { | |
719 | viewsPerDay = viewsPerDayString.split(',') | |
720 | .map(v => { | |
721 | const [ dateString, amount ] = v.split('|') | |
722 | ||
723 | return { | |
724 | date: new Date(dateString), | |
725 | views: +amount | |
726 | } | |
727 | }) | |
728 | } | |
8165d00a | 729 | |
50d6de9c | 730 | const actor = this.Actor.toFormattedJSON() |
6b738c7a | 731 | const videoChannel = { |
3fd3ab2d | 732 | id: this.id, |
749c7247 | 733 | displayName: this.getDisplayName(), |
3fd3ab2d | 734 | description: this.description, |
2422c46b | 735 | support: this.support, |
50d6de9c | 736 | isLocal: this.Actor.isOwned(), |
e024fd6a | 737 | updatedAt: this.updatedAt, |
8165d00a | 738 | ownerAccount: undefined, |
1ba471c5 C |
739 | videosCount, |
740 | viewsPerDay | |
6b738c7a C |
741 | } |
742 | ||
a4f99a76 | 743 | if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON() |
72c7248b | 744 | |
6b738c7a | 745 | return Object.assign(actor, videoChannel) |
72c7248b C |
746 | } |
747 | ||
b5fecbf4 | 748 | toActivityPubObject (this: MChannelAP): ActivityPubActor { |
8424c402 | 749 | const obj = this.Actor.toActivityPubObject(this.name) |
50d6de9c C |
750 | |
751 | return Object.assign(obj, { | |
752 | summary: this.description, | |
2422c46b | 753 | support: this.support, |
50d6de9c C |
754 | attributedTo: [ |
755 | { | |
756 | type: 'Person' as 'Person', | |
757 | id: this.Account.Actor.url | |
758 | } | |
759 | ] | |
760 | }) | |
72c7248b | 761 | } |
749c7247 | 762 | |
a59f210f C |
763 | getLocalUrl (this: MAccountActor | MChannelActor) { |
764 | return WEBSERVER.URL + `/video-channels/` + this.Actor.preferredUsername | |
765 | } | |
766 | ||
749c7247 C |
767 | getDisplayName () { |
768 | return this.name | |
769 | } | |
744d0eca C |
770 | |
771 | isOutdated () { | |
772 | return this.Actor.isOutdated() | |
773 | } | |
e024fd6a | 774 | |
3419e0e1 | 775 | setAsUpdated (transaction?: Transaction) { |
e024fd6a C |
776 | return setAsUpdated('videoChannel', this.id, transaction) |
777 | } | |
72c7248b | 778 | } |