]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/video/video-channel.ts
Merge branch 'release/4.0.0' into develop
[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'
a59f210f 20import { MAccountActor } from '@server/types/models'
b033851f 21import { AttributesOnly, pick } from '@shared/core-utils'
50d6de9c 22import { ActivityPubActor } from '../../../shared/models/activitypub'
418d092a 23import { VideoChannel, VideoChannelSummary } from '../../../shared/models/videos'
2422c46b 24import {
06a05d5f 25 isVideoChannelDescriptionValid,
27db7840 26 isVideoChannelDisplayNameValid,
2422c46b
C
27 isVideoChannelSupportValid
28} from '../../helpers/custom-validators/video-channels'
754b6f5f 29import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
2af337c8 30import { sendDeleteActor } from '../../lib/activitypub/send'
453e83ea 31import {
453e83ea 32 MChannelActor,
b5fecbf4 33 MChannelAP,
2cb03dc1 34 MChannelBannerAccountDefault,
b5fecbf4
C
35 MChannelFormattable,
36 MChannelSummaryFormattable
26d6bf65 37} from '../../types/models/video'
2af337c8 38import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account'
7d9ba5c0
C
39import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor'
40import { ActorFollowModel } from '../actor/actor-follow'
41import { ActorImageModel } from '../actor/actor-image'
2af337c8 42import { ServerModel } from '../server/server'
fa47956e 43import { setAsUpdated } from '../shared'
2af337c8
C
44import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils'
45import { VideoModel } from './video'
46import { VideoPlaylistModel } from './video-playlist'
754b6f5f 47import { CONFIG } from '@server/initializers/config'
f37dc0dd 48
418d092a 49export 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
59type AvailableForListOptions = {
60 actorId: number
bc99dfe5 61 search?: string
fa47956e 62 host?: string
b033851f 63 handles?: string[]
f37dc0dd
C
64}
65
8165d00a
RK
66type AvailableWithStatsOptions = {
67 daysPrior: number
68}
69
bfbd9128 70export 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 309export 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 = `
410SELECT COUNT(DISTINCT("VideoChannelModel"."id")) AS "count"
411FROM "videoChannel" AS "VideoChannelModel"
412INNER JOIN "video" AS "Videos"
413ON "VideoChannelModel"."id" = "Videos"."channelId"
414AND ("Videos"."publishedAt" > Now() - interval '${days}d')
415INNER JOIN "account" AS "Account"
416ON "VideoChannelModel"."accountId" = "Account"."id"
417INNER JOIN "actor" AS "Account->Actor"
418ON "Account"."actorId" = "Account->Actor"."id"
419AND "Account->Actor"."serverId" IS NULL
420LEFT OUTER JOIN "server" AS "Account->Actor->Server"
421ON "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}