aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/video/video-channel.ts
diff options
context:
space:
mode:
authorkontrollanten <6680299+kontrollanten@users.noreply.github.com>2022-02-28 08:34:43 +0100
committerGitHub <noreply@github.com>2022-02-28 08:34:43 +0100
commitd0800f7661f13fabe7bb6f4aa0ea50764f106405 (patch)
treed43e6b0b6f4a5a32e03487e6464edbcaf288be2a /server/models/video/video-channel.ts
parent5cad2ca9db9b9d138f8a33058d10b94a9fd50c69 (diff)
downloadPeerTube-d0800f7661f13fabe7bb6f4aa0ea50764f106405.tar.gz
PeerTube-d0800f7661f13fabe7bb6f4aa0ea50764f106405.tar.zst
PeerTube-d0800f7661f13fabe7bb6f4aa0ea50764f106405.zip
Implement avatar miniatures (#4639)
* client: remove unused file * refactor(client/my-actor-avatar): size from input Read size from component input instead of scss, to make it possible to use smaller avatar images when implemented. * implement avatar miniatures close #4560 * fix(test): max file size * fix(search-index): normalize res acc to avatarMini * refactor avatars to an array * client/search: resize channel avatar to 120 * refactor(client/videos): remove unused function * client(actor-avatar): set default size * fix tests and avatars full result When findOne is used only an array containting one avatar is returned. * update migration version and version notations * server/search: harmonize normalizing * Cleanup avatar miniature PR Co-authored-by: Chocobozzz <me@florianbigard.com>
Diffstat (limited to 'server/models/video/video-channel.ts')
-rw-r--r--server/models/video/video-channel.ts209
1 files changed, 127 insertions, 82 deletions
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index 2c6669bcb..410fd6d3f 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -31,6 +31,7 @@ import {
31import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' 31import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
32import { sendDeleteActor } from '../../lib/activitypub/send' 32import { sendDeleteActor } from '../../lib/activitypub/send'
33import { 33import {
34 MChannel,
34 MChannelActor, 35 MChannelActor,
35 MChannelAP, 36 MChannelAP,
36 MChannelBannerAccountDefault, 37 MChannelBannerAccountDefault,
@@ -62,6 +63,7 @@ type AvailableForListOptions = {
62 search?: string 63 search?: string
63 host?: string 64 host?: string
64 handles?: string[] 65 handles?: string[]
66 forCount?: boolean
65} 67}
66 68
67type AvailableWithStatsOptions = { 69type AvailableWithStatsOptions = {
@@ -116,70 +118,91 @@ export type SummaryOptions = {
116 }) 118 })
117 } 119 }
118 120
119 let rootWhere: WhereOptions 121 if (Array.isArray(options.handles) && options.handles.length !== 0) {
120 if (options.handles) { 122 const or: string[] = []
121 const or: WhereOptions[] = []
122 123
123 for (const handle of options.handles || []) { 124 for (const handle of options.handles || []) {
124 const [ preferredUsername, host ] = handle.split('@') 125 const [ preferredUsername, host ] = handle.split('@')
125 126
126 if (!host || host === WEBSERVER.HOST) { 127 if (!host || host === WEBSERVER.HOST) {
127 or.push({ 128 or.push(`("preferredUsername" = ${VideoChannelModel.sequelize.escape(preferredUsername)} AND "serverId" IS NULL)`)
128 '$Actor.preferredUsername$': preferredUsername,
129 '$Actor.serverId$': null
130 })
131 } else { 129 } else {
132 or.push({ 130 or.push(
133 '$Actor.preferredUsername$': preferredUsername, 131 `(` +
134 '$Actor.Server.host$': host 132 `"preferredUsername" = ${VideoChannelModel.sequelize.escape(preferredUsername)} ` +
135 }) 133 `AND "host" = ${VideoChannelModel.sequelize.escape(host)}` +
134 `)`
135 )
136 } 136 }
137 } 137 }
138 138
139 rootWhere = { 139 whereActorAnd.push({
140 [Op.or]: or 140 id: {
141 } 141 [Op.in]: literal(`(SELECT "actor".id FROM actor LEFT JOIN server on server.id = actor."serverId" WHERE ${or.join(' OR ')})`)
142 }
143 })
144 }
145
146 const channelInclude: Includeable[] = []
147 const accountInclude: Includeable[] = []
148
149 if (options.forCount !== true) {
150 accountInclude.push({
151 model: ServerModel,
152 required: false
153 })
154
155 accountInclude.push({
156 model: ActorImageModel,
157 as: 'Avatars',
158 required: false
159 })
160
161 channelInclude.push({
162 model: ActorImageModel,
163 as: 'Avatars',
164 required: false
165 })
166
167 channelInclude.push({
168 model: ActorImageModel,
169 as: 'Banners',
170 required: false
171 })
172 }
173
174 if (options.forCount !== true || serverRequired) {
175 channelInclude.push({
176 model: ServerModel,
177 duplicating: false,
178 required: serverRequired,
179 where: whereServer
180 })
142 } 181 }
143 182
144 return { 183 return {
145 where: rootWhere,
146 include: [ 184 include: [
147 { 185 {
148 attributes: { 186 attributes: {
149 exclude: unusedActorAttributesForAPI 187 exclude: unusedActorAttributesForAPI
150 }, 188 },
151 model: ActorModel, 189 model: ActorModel.unscoped(),
152 where: { 190 where: {
153 [Op.and]: whereActorAnd 191 [Op.and]: whereActorAnd
154 }, 192 },
155 include: [ 193 include: channelInclude
156 {
157 model: ServerModel,
158 required: serverRequired,
159 where: whereServer
160 },
161 {
162 model: ActorImageModel,
163 as: 'Avatar',
164 required: false
165 },
166 {
167 model: ActorImageModel,
168 as: 'Banner',
169 required: false
170 }
171 ]
172 }, 194 },
173 { 195 {
174 model: AccountModel, 196 model: AccountModel.unscoped(),
175 required: true, 197 required: true,
176 include: [ 198 include: [
177 { 199 {
178 attributes: { 200 attributes: {
179 exclude: unusedActorAttributesForAPI 201 exclude: unusedActorAttributesForAPI
180 }, 202 },
181 model: ActorModel, // Default scope includes avatar and server 203 model: ActorModel.unscoped(),
182 required: true 204 required: true,
205 include: accountInclude
183 } 206 }
184 ] 207 ]
185 } 208 }
@@ -189,7 +212,7 @@ export type SummaryOptions = {
189 [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => { 212 [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => {
190 const include: Includeable[] = [ 213 const include: Includeable[] = [
191 { 214 {
192 attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], 215 attributes: [ 'id', 'preferredUsername', 'url', 'serverId' ],
193 model: ActorModel.unscoped(), 216 model: ActorModel.unscoped(),
194 required: options.actorRequired ?? true, 217 required: options.actorRequired ?? true,
195 include: [ 218 include: [
@@ -199,8 +222,8 @@ export type SummaryOptions = {
199 required: false 222 required: false
200 }, 223 },
201 { 224 {
202 model: ActorImageModel.unscoped(), 225 model: ActorImageModel,
203 as: 'Avatar', 226 as: 'Avatars',
204 required: false 227 required: false
205 } 228 }
206 ] 229 ]
@@ -245,7 +268,7 @@ export type SummaryOptions = {
245 { 268 {
246 model: ActorImageModel, 269 model: ActorImageModel,
247 required: false, 270 required: false,
248 as: 'Banner' 271 as: 'Banners'
249 } 272 }
250 ] 273 ]
251 } 274 }
@@ -474,14 +497,14 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
474 order: getSort(parameters.sort) 497 order: getSort(parameters.sort)
475 } 498 }
476 499
477 return VideoChannelModel 500 const getScope = (forCount: boolean) => {
478 .scope({ 501 return { method: [ ScopeNames.FOR_API, { actorId, forCount } as AvailableForListOptions ] }
479 method: [ ScopeNames.FOR_API, { actorId } as AvailableForListOptions ] 502 }
480 }) 503
481 .findAndCountAll(query) 504 return Promise.all([
482 .then(({ rows, count }) => { 505 VideoChannelModel.scope(getScope(true)).count(),
483 return { total: count, data: rows } 506 VideoChannelModel.scope(getScope(false)).findAll(query)
484 }) 507 ]).then(([ total, data ]) => ({ total, data }))
485 } 508 }
486 509
487 static searchForApi (options: Pick<AvailableForListOptions, 'actorId' | 'search' | 'host' | 'handles'> & { 510 static searchForApi (options: Pick<AvailableForListOptions, 'actorId' | 'search' | 'host' | 'handles'> & {
@@ -519,14 +542,22 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
519 where 542 where
520 } 543 }
521 544
522 return VideoChannelModel 545 const getScope = (forCount: boolean) => {
523 .scope({ 546 return {
524 method: [ ScopeNames.FOR_API, pick(options, [ 'actorId', 'host', 'handles' ]) as AvailableForListOptions ] 547 method: [
525 }) 548 ScopeNames.FOR_API, {
526 .findAndCountAll(query) 549 ...pick(options, [ 'actorId', 'host', 'handles' ]),
527 .then(({ rows, count }) => { 550
528 return { total: count, data: rows } 551 forCount
529 }) 552 } as AvailableForListOptions
553 ]
554 }
555 }
556
557 return Promise.all([
558 VideoChannelModel.scope(getScope(true)).count(query),
559 VideoChannelModel.scope(getScope(false)).findAll(query)
560 ]).then(([ total, data ]) => ({ total, data }))
530 } 561 }
531 562
532 static listByAccountForAPI (options: { 563 static listByAccountForAPI (options: {
@@ -552,20 +583,26 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
552 } 583 }
553 : null 584 : null
554 585
555 const query = { 586 const getQuery = (forCount: boolean) => {
556 offset: options.start, 587 const accountModel = forCount
557 limit: options.count, 588 ? AccountModel.unscoped()
558 order: getSort(options.sort), 589 : AccountModel
559 include: [ 590
560 { 591 return {
561 model: AccountModel, 592 offset: options.start,
562 where: { 593 limit: options.count,
563 id: options.accountId 594 order: getSort(options.sort),
564 }, 595 include: [
565 required: true 596 {
566 } 597 model: accountModel,
567 ], 598 where: {
568 where 599 id: options.accountId
600 },
601 required: true
602 }
603 ],
604 where
605 }
569 } 606 }
570 607
571 const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR_BANNER ] 608 const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR_BANNER ]
@@ -576,21 +613,19 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
576 }) 613 })
577 } 614 }
578 615
579 return VideoChannelModel 616 return Promise.all([
580 .scope(scopes) 617 VideoChannelModel.scope(scopes).count(getQuery(true)),
581 .findAndCountAll(query) 618 VideoChannelModel.scope(scopes).findAll(getQuery(false))
582 .then(({ rows, count }) => { 619 ]).then(([ total, data ]) => ({ total, data }))
583 return { total: count, data: rows }
584 })
585 } 620 }
586 621
587 static listAllByAccount (accountId: number) { 622 static listAllByAccount (accountId: number): Promise<MChannel[]> {
588 const query = { 623 const query = {
589 limit: CONFIG.VIDEO_CHANNELS.MAX_PER_USER, 624 limit: CONFIG.VIDEO_CHANNELS.MAX_PER_USER,
590 include: [ 625 include: [
591 { 626 {
592 attributes: [], 627 attributes: [],
593 model: AccountModel, 628 model: AccountModel.unscoped(),
594 where: { 629 where: {
595 id: accountId 630 id: accountId
596 }, 631 },
@@ -621,7 +656,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
621 { 656 {
622 model: ActorImageModel, 657 model: ActorImageModel,
623 required: false, 658 required: false,
624 as: 'Banner' 659 as: 'Banners'
625 } 660 }
626 ] 661 ]
627 } 662 }
@@ -655,7 +690,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
655 { 690 {
656 model: ActorImageModel, 691 model: ActorImageModel,
657 required: false, 692 required: false,
658 as: 'Banner' 693 as: 'Banners'
659 } 694 }
660 ] 695 ]
661 } 696 }
@@ -685,7 +720,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
685 { 720 {
686 model: ActorImageModel, 721 model: ActorImageModel,
687 required: false, 722 required: false,
688 as: 'Banner' 723 as: 'Banners'
689 } 724 }
690 ] 725 ]
691 } 726 }
@@ -706,6 +741,9 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
706 displayName: this.getDisplayName(), 741 displayName: this.getDisplayName(),
707 url: actor.url, 742 url: actor.url,
708 host: actor.host, 743 host: actor.host,
744 avatars: actor.avatars,
745
746 // TODO: remove, deprecated in 4.2
709 avatar: actor.avatar 747 avatar: actor.avatar
710 } 748 }
711 } 749 }
@@ -736,9 +774,16 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
736 support: this.support, 774 support: this.support,
737 isLocal: this.Actor.isOwned(), 775 isLocal: this.Actor.isOwned(),
738 updatedAt: this.updatedAt, 776 updatedAt: this.updatedAt,
777
739 ownerAccount: undefined, 778 ownerAccount: undefined,
779
740 videosCount, 780 videosCount,
741 viewsPerDay 781 viewsPerDay,
782
783 avatars: actor.avatars,
784
785 // TODO: remove, deprecated in 4.2
786 avatar: actor.avatar
742 } 787 }
743 788
744 if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON() 789 if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON()