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