diff options
Diffstat (limited to 'server/models')
-rw-r--r-- | server/models/account/account.ts | 5 | ||||
-rw-r--r-- | server/models/account/actor-image.ts | 100 | ||||
-rw-r--r-- | server/models/account/user-notification-setting.ts | 26 | ||||
-rw-r--r-- | server/models/account/user-notification.ts | 98 | ||||
-rw-r--r-- | server/models/account/user.ts | 20 | ||||
-rw-r--r-- | server/models/activitypub/actor-follow.ts | 7 | ||||
-rw-r--r-- | server/models/activitypub/actor.ts | 87 | ||||
-rw-r--r-- | server/models/application/application.ts | 4 | ||||
-rw-r--r-- | server/models/avatar/avatar.ts | 81 | ||||
-rw-r--r-- | server/models/oauth/oauth-token.ts | 11 | ||||
-rw-r--r-- | server/models/redundancy/video-redundancy.ts | 9 | ||||
-rw-r--r-- | server/models/utils.ts | 8 | ||||
-rw-r--r-- | server/models/video/video-channel.ts | 145 | ||||
-rw-r--r-- | server/models/video/video-playlist.ts | 60 | ||||
-rw-r--r-- | server/models/video/video-query-builder.ts | 5 | ||||
-rw-r--r-- | server/models/video/video.ts | 25 |
16 files changed, 486 insertions, 205 deletions
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index c72f9c63d..312451abe 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -33,7 +33,7 @@ import { | |||
33 | import { ActorModel } from '../activitypub/actor' | 33 | import { ActorModel } from '../activitypub/actor' |
34 | import { ActorFollowModel } from '../activitypub/actor-follow' | 34 | import { ActorFollowModel } from '../activitypub/actor-follow' |
35 | import { ApplicationModel } from '../application/application' | 35 | import { ApplicationModel } from '../application/application' |
36 | import { AvatarModel } from '../avatar/avatar' | 36 | import { ActorImageModel } from './actor-image' |
37 | import { ServerModel } from '../server/server' | 37 | import { ServerModel } from '../server/server' |
38 | import { ServerBlocklistModel } from '../server/server-blocklist' | 38 | import { ServerBlocklistModel } from '../server/server-blocklist' |
39 | import { getSort, throwIfNotValid } from '../utils' | 39 | import { getSort, throwIfNotValid } from '../utils' |
@@ -82,7 +82,8 @@ export type SummaryOptions = { | |||
82 | serverInclude, | 82 | serverInclude, |
83 | 83 | ||
84 | { | 84 | { |
85 | model: AvatarModel.unscoped(), | 85 | model: ActorImageModel.unscoped(), |
86 | as: 'Avatar', | ||
86 | required: false | 87 | required: false |
87 | } | 88 | } |
88 | ] | 89 | ] |
diff --git a/server/models/account/actor-image.ts b/server/models/account/actor-image.ts new file mode 100644 index 000000000..ae05b4969 --- /dev/null +++ b/server/models/account/actor-image.ts | |||
@@ -0,0 +1,100 @@ | |||
1 | import { remove } from 'fs-extra' | ||
2 | import { join } from 'path' | ||
3 | import { AfterDestroy, AllowNull, Column, CreatedAt, Default, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
4 | import { MActorImageFormattable } from '@server/types/models' | ||
5 | import { ActorImageType } from '@shared/models' | ||
6 | import { ActorImage } from '../../../shared/models/actors/actor-image.model' | ||
7 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | ||
8 | import { logger } from '../../helpers/logger' | ||
9 | import { CONFIG } from '../../initializers/config' | ||
10 | import { LAZY_STATIC_PATHS } from '../../initializers/constants' | ||
11 | import { throwIfNotValid } from '../utils' | ||
12 | |||
13 | @Table({ | ||
14 | tableName: 'actorImage', | ||
15 | indexes: [ | ||
16 | { | ||
17 | fields: [ 'filename' ], | ||
18 | unique: true | ||
19 | } | ||
20 | ] | ||
21 | }) | ||
22 | export class ActorImageModel extends Model { | ||
23 | |||
24 | @AllowNull(false) | ||
25 | @Column | ||
26 | filename: string | ||
27 | |||
28 | @AllowNull(true) | ||
29 | @Default(null) | ||
30 | @Column | ||
31 | height: number | ||
32 | |||
33 | @AllowNull(true) | ||
34 | @Default(null) | ||
35 | @Column | ||
36 | width: number | ||
37 | |||
38 | @AllowNull(true) | ||
39 | @Is('ActorImageFileUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'fileUrl', true)) | ||
40 | @Column | ||
41 | fileUrl: string | ||
42 | |||
43 | @AllowNull(false) | ||
44 | @Column | ||
45 | onDisk: boolean | ||
46 | |||
47 | @AllowNull(false) | ||
48 | @Column | ||
49 | type: ActorImageType | ||
50 | |||
51 | @CreatedAt | ||
52 | createdAt: Date | ||
53 | |||
54 | @UpdatedAt | ||
55 | updatedAt: Date | ||
56 | |||
57 | @AfterDestroy | ||
58 | static removeFilesAndSendDelete (instance: ActorImageModel) { | ||
59 | logger.info('Removing actor image file %s.', instance.filename) | ||
60 | |||
61 | // Don't block the transaction | ||
62 | instance.removeImage() | ||
63 | .catch(err => logger.error('Cannot remove actor image file %s.', instance.filename, err)) | ||
64 | } | ||
65 | |||
66 | static loadByName (filename: string) { | ||
67 | const query = { | ||
68 | where: { | ||
69 | filename | ||
70 | } | ||
71 | } | ||
72 | |||
73 | return ActorImageModel.findOne(query) | ||
74 | } | ||
75 | |||
76 | toFormattedJSON (this: MActorImageFormattable): ActorImage { | ||
77 | return { | ||
78 | path: this.getStaticPath(), | ||
79 | createdAt: this.createdAt, | ||
80 | updatedAt: this.updatedAt | ||
81 | } | ||
82 | } | ||
83 | |||
84 | getStaticPath () { | ||
85 | if (this.type === ActorImageType.AVATAR) { | ||
86 | return join(LAZY_STATIC_PATHS.AVATARS, this.filename) | ||
87 | } | ||
88 | |||
89 | return join(LAZY_STATIC_PATHS.BANNERS, this.filename) | ||
90 | } | ||
91 | |||
92 | getPath () { | ||
93 | return join(CONFIG.STORAGE.ACTOR_IMAGES, this.filename) | ||
94 | } | ||
95 | |||
96 | removeImage () { | ||
97 | const imagePath = join(CONFIG.STORAGE.ACTOR_IMAGES, this.filename) | ||
98 | return remove(imagePath) | ||
99 | } | ||
100 | } | ||
diff --git a/server/models/account/user-notification-setting.ts b/server/models/account/user-notification-setting.ts index ebab8b6d2..138051528 100644 --- a/server/models/account/user-notification-setting.ts +++ b/server/models/account/user-notification-setting.ts | |||
@@ -12,10 +12,10 @@ import { | |||
12 | Table, | 12 | Table, |
13 | UpdatedAt | 13 | UpdatedAt |
14 | } from 'sequelize-typescript' | 14 | } from 'sequelize-typescript' |
15 | import { TokensCache } from '@server/lib/auth/tokens-cache' | ||
15 | import { MNotificationSettingFormattable } from '@server/types/models' | 16 | import { MNotificationSettingFormattable } from '@server/types/models' |
16 | import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' | 17 | import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' |
17 | import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' | 18 | import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' |
18 | import { clearCacheByUserId } from '../../lib/oauth-model' | ||
19 | import { throwIfNotValid } from '../utils' | 19 | import { throwIfNotValid } from '../utils' |
20 | import { UserModel } from './user' | 20 | import { UserModel } from './user' |
21 | 21 | ||
@@ -156,6 +156,24 @@ export class UserNotificationSettingModel extends Model { | |||
156 | @Column | 156 | @Column |
157 | abuseNewMessage: UserNotificationSettingValue | 157 | abuseNewMessage: UserNotificationSettingValue |
158 | 158 | ||
159 | @AllowNull(false) | ||
160 | @Default(null) | ||
161 | @Is( | ||
162 | 'UserNotificationSettingNewPeerTubeVersion', | ||
163 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'newPeerTubeVersion') | ||
164 | ) | ||
165 | @Column | ||
166 | newPeerTubeVersion: UserNotificationSettingValue | ||
167 | |||
168 | @AllowNull(false) | ||
169 | @Default(null) | ||
170 | @Is( | ||
171 | 'UserNotificationSettingNewPeerPluginVersion', | ||
172 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'newPluginVersion') | ||
173 | ) | ||
174 | @Column | ||
175 | newPluginVersion: UserNotificationSettingValue | ||
176 | |||
159 | @ForeignKey(() => UserModel) | 177 | @ForeignKey(() => UserModel) |
160 | @Column | 178 | @Column |
161 | userId: number | 179 | userId: number |
@@ -177,7 +195,7 @@ export class UserNotificationSettingModel extends Model { | |||
177 | @AfterUpdate | 195 | @AfterUpdate |
178 | @AfterDestroy | 196 | @AfterDestroy |
179 | static removeTokenCache (instance: UserNotificationSettingModel) { | 197 | static removeTokenCache (instance: UserNotificationSettingModel) { |
180 | return clearCacheByUserId(instance.userId) | 198 | return TokensCache.Instance.clearCacheByUserId(instance.userId) |
181 | } | 199 | } |
182 | 200 | ||
183 | toFormattedJSON (this: MNotificationSettingFormattable): UserNotificationSetting { | 201 | toFormattedJSON (this: MNotificationSettingFormattable): UserNotificationSetting { |
@@ -195,7 +213,9 @@ export class UserNotificationSettingModel extends Model { | |||
195 | newInstanceFollower: this.newInstanceFollower, | 213 | newInstanceFollower: this.newInstanceFollower, |
196 | autoInstanceFollowing: this.autoInstanceFollowing, | 214 | autoInstanceFollowing: this.autoInstanceFollowing, |
197 | abuseNewMessage: this.abuseNewMessage, | 215 | abuseNewMessage: this.abuseNewMessage, |
198 | abuseStateChange: this.abuseStateChange | 216 | abuseStateChange: this.abuseStateChange, |
217 | newPeerTubeVersion: this.newPeerTubeVersion, | ||
218 | newPluginVersion: this.newPluginVersion | ||
199 | } | 219 | } |
200 | } | 220 | } |
201 | } | 221 | } |
diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts index add129644..805095002 100644 --- a/server/models/account/user-notification.ts +++ b/server/models/account/user-notification.ts | |||
@@ -9,7 +9,8 @@ import { VideoAbuseModel } from '../abuse/video-abuse' | |||
9 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' | 9 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' |
10 | import { ActorModel } from '../activitypub/actor' | 10 | import { ActorModel } from '../activitypub/actor' |
11 | import { ActorFollowModel } from '../activitypub/actor-follow' | 11 | import { ActorFollowModel } from '../activitypub/actor-follow' |
12 | import { AvatarModel } from '../avatar/avatar' | 12 | import { ApplicationModel } from '../application/application' |
13 | import { PluginModel } from '../server/plugin' | ||
13 | import { ServerModel } from '../server/server' | 14 | import { ServerModel } from '../server/server' |
14 | import { getSort, throwIfNotValid } from '../utils' | 15 | import { getSort, throwIfNotValid } from '../utils' |
15 | import { VideoModel } from '../video/video' | 16 | import { VideoModel } from '../video/video' |
@@ -18,6 +19,7 @@ import { VideoChannelModel } from '../video/video-channel' | |||
18 | import { VideoCommentModel } from '../video/video-comment' | 19 | import { VideoCommentModel } from '../video/video-comment' |
19 | import { VideoImportModel } from '../video/video-import' | 20 | import { VideoImportModel } from '../video/video-import' |
20 | import { AccountModel } from './account' | 21 | import { AccountModel } from './account' |
22 | import { ActorImageModel } from './actor-image' | ||
21 | import { UserModel } from './user' | 23 | import { UserModel } from './user' |
22 | 24 | ||
23 | enum ScopeNames { | 25 | enum ScopeNames { |
@@ -32,7 +34,8 @@ function buildActorWithAvatarInclude () { | |||
32 | include: [ | 34 | include: [ |
33 | { | 35 | { |
34 | attributes: [ 'filename' ], | 36 | attributes: [ 'filename' ], |
35 | model: AvatarModel.unscoped(), | 37 | as: 'Avatar', |
38 | model: ActorImageModel.unscoped(), | ||
36 | required: false | 39 | required: false |
37 | }, | 40 | }, |
38 | { | 41 | { |
@@ -96,7 +99,7 @@ function buildAccountInclude (required: boolean, withActor = false) { | |||
96 | attributes: [ 'id' ], | 99 | attributes: [ 'id' ], |
97 | model: VideoAbuseModel.unscoped(), | 100 | model: VideoAbuseModel.unscoped(), |
98 | required: false, | 101 | required: false, |
99 | include: [ buildVideoInclude(true) ] | 102 | include: [ buildVideoInclude(false) ] |
100 | }, | 103 | }, |
101 | { | 104 | { |
102 | attributes: [ 'id' ], | 105 | attributes: [ 'id' ], |
@@ -106,12 +109,12 @@ function buildAccountInclude (required: boolean, withActor = false) { | |||
106 | { | 109 | { |
107 | attributes: [ 'id', 'originCommentId' ], | 110 | attributes: [ 'id', 'originCommentId' ], |
108 | model: VideoCommentModel.unscoped(), | 111 | model: VideoCommentModel.unscoped(), |
109 | required: true, | 112 | required: false, |
110 | include: [ | 113 | include: [ |
111 | { | 114 | { |
112 | attributes: [ 'id', 'name', 'uuid' ], | 115 | attributes: [ 'id', 'name', 'uuid' ], |
113 | model: VideoModel.unscoped(), | 116 | model: VideoModel.unscoped(), |
114 | required: true | 117 | required: false |
115 | } | 118 | } |
116 | ] | 119 | ] |
117 | } | 120 | } |
@@ -120,7 +123,7 @@ function buildAccountInclude (required: boolean, withActor = false) { | |||
120 | { | 123 | { |
121 | model: AccountModel, | 124 | model: AccountModel, |
122 | as: 'FlaggedAccount', | 125 | as: 'FlaggedAccount', |
123 | required: true, | 126 | required: false, |
124 | include: [ buildActorWithAvatarInclude() ] | 127 | include: [ buildActorWithAvatarInclude() ] |
125 | } | 128 | } |
126 | ] | 129 | ] |
@@ -141,6 +144,18 @@ function buildAccountInclude (required: boolean, withActor = false) { | |||
141 | }, | 144 | }, |
142 | 145 | ||
143 | { | 146 | { |
147 | attributes: [ 'id', 'name', 'type', 'latestVersion' ], | ||
148 | model: PluginModel.unscoped(), | ||
149 | required: false | ||
150 | }, | ||
151 | |||
152 | { | ||
153 | attributes: [ 'id', 'latestPeerTubeVersion' ], | ||
154 | model: ApplicationModel.unscoped(), | ||
155 | required: false | ||
156 | }, | ||
157 | |||
158 | { | ||
144 | attributes: [ 'id', 'state' ], | 159 | attributes: [ 'id', 'state' ], |
145 | model: ActorFollowModel.unscoped(), | 160 | model: ActorFollowModel.unscoped(), |
146 | required: false, | 161 | required: false, |
@@ -158,7 +173,8 @@ function buildAccountInclude (required: boolean, withActor = false) { | |||
158 | }, | 173 | }, |
159 | { | 174 | { |
160 | attributes: [ 'filename' ], | 175 | attributes: [ 'filename' ], |
161 | model: AvatarModel.unscoped(), | 176 | as: 'Avatar', |
177 | model: ActorImageModel.unscoped(), | ||
162 | required: false | 178 | required: false |
163 | }, | 179 | }, |
164 | { | 180 | { |
@@ -251,6 +267,22 @@ function buildAccountInclude (required: boolean, withActor = false) { | |||
251 | [Op.ne]: null | 267 | [Op.ne]: null |
252 | } | 268 | } |
253 | } | 269 | } |
270 | }, | ||
271 | { | ||
272 | fields: [ 'pluginId' ], | ||
273 | where: { | ||
274 | pluginId: { | ||
275 | [Op.ne]: null | ||
276 | } | ||
277 | } | ||
278 | }, | ||
279 | { | ||
280 | fields: [ 'applicationId' ], | ||
281 | where: { | ||
282 | applicationId: { | ||
283 | [Op.ne]: null | ||
284 | } | ||
285 | } | ||
254 | } | 286 | } |
255 | ] as (ModelIndexesOptions & { where?: WhereOptions })[] | 287 | ] as (ModelIndexesOptions & { where?: WhereOptions })[] |
256 | }) | 288 | }) |
@@ -370,6 +402,30 @@ export class UserNotificationModel extends Model { | |||
370 | }) | 402 | }) |
371 | ActorFollow: ActorFollowModel | 403 | ActorFollow: ActorFollowModel |
372 | 404 | ||
405 | @ForeignKey(() => PluginModel) | ||
406 | @Column | ||
407 | pluginId: number | ||
408 | |||
409 | @BelongsTo(() => PluginModel, { | ||
410 | foreignKey: { | ||
411 | allowNull: true | ||
412 | }, | ||
413 | onDelete: 'cascade' | ||
414 | }) | ||
415 | Plugin: PluginModel | ||
416 | |||
417 | @ForeignKey(() => ApplicationModel) | ||
418 | @Column | ||
419 | applicationId: number | ||
420 | |||
421 | @BelongsTo(() => ApplicationModel, { | ||
422 | foreignKey: { | ||
423 | allowNull: true | ||
424 | }, | ||
425 | onDelete: 'cascade' | ||
426 | }) | ||
427 | Application: ApplicationModel | ||
428 | |||
373 | static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { | 429 | static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { |
374 | const where = { userId } | 430 | const where = { userId } |
375 | 431 | ||
@@ -524,6 +580,18 @@ export class UserNotificationModel extends Model { | |||
524 | } | 580 | } |
525 | : undefined | 581 | : undefined |
526 | 582 | ||
583 | const plugin = this.Plugin | ||
584 | ? { | ||
585 | name: this.Plugin.name, | ||
586 | type: this.Plugin.type, | ||
587 | latestVersion: this.Plugin.latestVersion | ||
588 | } | ||
589 | : undefined | ||
590 | |||
591 | const peertube = this.Application | ||
592 | ? { latestVersion: this.Application.latestPeerTubeVersion } | ||
593 | : undefined | ||
594 | |||
527 | return { | 595 | return { |
528 | id: this.id, | 596 | id: this.id, |
529 | type: this.type, | 597 | type: this.type, |
@@ -535,6 +603,8 @@ export class UserNotificationModel extends Model { | |||
535 | videoBlacklist, | 603 | videoBlacklist, |
536 | account, | 604 | account, |
537 | actorFollow, | 605 | actorFollow, |
606 | plugin, | ||
607 | peertube, | ||
538 | createdAt: this.createdAt.toISOString(), | 608 | createdAt: this.createdAt.toISOString(), |
539 | updatedAt: this.updatedAt.toISOString() | 609 | updatedAt: this.updatedAt.toISOString() |
540 | } | 610 | } |
@@ -553,17 +623,19 @@ export class UserNotificationModel extends Model { | |||
553 | ? { | 623 | ? { |
554 | threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(), | 624 | threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(), |
555 | 625 | ||
556 | video: { | 626 | video: abuse.VideoCommentAbuse.VideoComment.Video |
557 | id: abuse.VideoCommentAbuse.VideoComment.Video.id, | 627 | ? { |
558 | name: abuse.VideoCommentAbuse.VideoComment.Video.name, | 628 | id: abuse.VideoCommentAbuse.VideoComment.Video.id, |
559 | uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid | 629 | name: abuse.VideoCommentAbuse.VideoComment.Video.name, |
560 | } | 630 | uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid |
631 | } | ||
632 | : undefined | ||
561 | } | 633 | } |
562 | : undefined | 634 | : undefined |
563 | 635 | ||
564 | const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined | 636 | const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined |
565 | 637 | ||
566 | const accountAbuse = (!commentAbuse && !videoAbuse) ? this.formatActor(abuse.FlaggedAccount) : undefined | 638 | const accountAbuse = (!commentAbuse && !videoAbuse && abuse.FlaggedAccount) ? this.formatActor(abuse.FlaggedAccount) : undefined |
567 | 639 | ||
568 | return { | 640 | return { |
569 | id: abuse.id, | 641 | id: abuse.id, |
diff --git a/server/models/account/user.ts b/server/models/account/user.ts index c1f22b76a..00c6d73aa 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts | |||
@@ -21,6 +21,7 @@ import { | |||
21 | Table, | 21 | Table, |
22 | UpdatedAt | 22 | UpdatedAt |
23 | } from 'sequelize-typescript' | 23 | } from 'sequelize-typescript' |
24 | import { TokensCache } from '@server/lib/auth/tokens-cache' | ||
24 | import { | 25 | import { |
25 | MMyUserFormattable, | 26 | MMyUserFormattable, |
26 | MUser, | 27 | MUser, |
@@ -58,7 +59,6 @@ import { | |||
58 | } from '../../helpers/custom-validators/users' | 59 | } from '../../helpers/custom-validators/users' |
59 | import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' | 60 | import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' |
60 | import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants' | 61 | import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants' |
61 | import { clearCacheByUserId } from '../../lib/oauth-model' | ||
62 | import { getThemeOrDefault } from '../../lib/plugins/theme-utils' | 62 | import { getThemeOrDefault } from '../../lib/plugins/theme-utils' |
63 | import { ActorModel } from '../activitypub/actor' | 63 | import { ActorModel } from '../activitypub/actor' |
64 | import { ActorFollowModel } from '../activitypub/actor-follow' | 64 | import { ActorFollowModel } from '../activitypub/actor-follow' |
@@ -71,6 +71,7 @@ import { VideoLiveModel } from '../video/video-live' | |||
71 | import { VideoPlaylistModel } from '../video/video-playlist' | 71 | import { VideoPlaylistModel } from '../video/video-playlist' |
72 | import { AccountModel } from './account' | 72 | import { AccountModel } from './account' |
73 | import { UserNotificationSettingModel } from './user-notification-setting' | 73 | import { UserNotificationSettingModel } from './user-notification-setting' |
74 | import { ActorImageModel } from './actor-image' | ||
74 | 75 | ||
75 | enum ScopeNames { | 76 | enum ScopeNames { |
76 | FOR_ME_API = 'FOR_ME_API', | 77 | FOR_ME_API = 'FOR_ME_API', |
@@ -97,7 +98,20 @@ enum ScopeNames { | |||
97 | model: AccountModel, | 98 | model: AccountModel, |
98 | include: [ | 99 | include: [ |
99 | { | 100 | { |
100 | model: VideoChannelModel | 101 | model: VideoChannelModel.unscoped(), |
102 | include: [ | ||
103 | { | ||
104 | model: ActorModel, | ||
105 | required: true, | ||
106 | include: [ | ||
107 | { | ||
108 | model: ActorImageModel, | ||
109 | as: 'Banner', | ||
110 | required: false | ||
111 | } | ||
112 | ] | ||
113 | } | ||
114 | ] | ||
101 | }, | 115 | }, |
102 | { | 116 | { |
103 | attributes: [ 'id', 'name', 'type' ], | 117 | attributes: [ 'id', 'name', 'type' ], |
@@ -411,7 +425,7 @@ export class UserModel extends Model { | |||
411 | @AfterUpdate | 425 | @AfterUpdate |
412 | @AfterDestroy | 426 | @AfterDestroy |
413 | static removeTokenCache (instance: UserModel) { | 427 | static removeTokenCache (instance: UserModel) { |
414 | return clearCacheByUserId(instance.id) | 428 | return TokensCache.Instance.clearCacheByUserId(instance.id) |
415 | } | 429 | } |
416 | 430 | ||
417 | static countTotal () { | 431 | static countTotal () { |
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts index ce6a4e267..4c5f37620 100644 --- a/server/models/activitypub/actor-follow.ts +++ b/server/models/activitypub/actor-follow.ts | |||
@@ -248,13 +248,6 @@ export class ActorFollowModel extends Model { | |||
248 | } | 248 | } |
249 | 249 | ||
250 | return ActorFollowModel.findOne(query) | 250 | return ActorFollowModel.findOne(query) |
251 | .then(result => { | ||
252 | if (result?.ActorFollowing.VideoChannel) { | ||
253 | result.ActorFollowing.VideoChannel.Actor = result.ActorFollowing | ||
254 | } | ||
255 | |||
256 | return result | ||
257 | }) | ||
258 | } | 251 | } |
259 | 252 | ||
260 | static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]): Promise<MActorFollowFollowingHost[]> { | 253 | static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]): Promise<MActorFollowFollowingHost[]> { |
diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts index 3b98e8841..19f3f7e04 100644 --- a/server/models/activitypub/actor.ts +++ b/server/models/activitypub/actor.ts | |||
@@ -19,7 +19,7 @@ import { | |||
19 | } from 'sequelize-typescript' | 19 | } from 'sequelize-typescript' |
20 | import { ModelCache } from '@server/models/model-cache' | 20 | import { ModelCache } from '@server/models/model-cache' |
21 | import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub' | 21 | import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub' |
22 | import { Avatar } from '../../../shared/models/avatars/avatar.model' | 22 | import { ActorImage } from '../../../shared/models/actors/actor-image.model' |
23 | import { activityPubContextify } from '../../helpers/activitypub' | 23 | import { activityPubContextify } from '../../helpers/activitypub' |
24 | import { | 24 | import { |
25 | isActorFollowersCountValid, | 25 | isActorFollowersCountValid, |
@@ -29,11 +29,19 @@ import { | |||
29 | isActorPublicKeyValid | 29 | isActorPublicKeyValid |
30 | } from '../../helpers/custom-validators/activitypub/actor' | 30 | } from '../../helpers/custom-validators/activitypub/actor' |
31 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 31 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
32 | import { ACTIVITY_PUB, ACTIVITY_PUB_ACTOR_TYPES, CONSTRAINTS_FIELDS, SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants' | 32 | import { |
33 | ACTIVITY_PUB, | ||
34 | ACTIVITY_PUB_ACTOR_TYPES, | ||
35 | CONSTRAINTS_FIELDS, | ||
36 | MIMETYPES, | ||
37 | SERVER_ACTOR_NAME, | ||
38 | WEBSERVER | ||
39 | } from '../../initializers/constants' | ||
33 | import { | 40 | import { |
34 | MActor, | 41 | MActor, |
35 | MActorAccountChannelId, | 42 | MActorAccountChannelId, |
36 | MActorAP, | 43 | MActorAPAccount, |
44 | MActorAPChannel, | ||
37 | MActorFormattable, | 45 | MActorFormattable, |
38 | MActorFull, | 46 | MActorFull, |
39 | MActorHost, | 47 | MActorHost, |
@@ -43,7 +51,7 @@ import { | |||
43 | MActorWithInboxes | 51 | MActorWithInboxes |
44 | } from '../../types/models' | 52 | } from '../../types/models' |
45 | import { AccountModel } from '../account/account' | 53 | import { AccountModel } from '../account/account' |
46 | import { AvatarModel } from '../avatar/avatar' | 54 | import { ActorImageModel } from '../account/actor-image' |
47 | import { ServerModel } from '../server/server' | 55 | import { ServerModel } from '../server/server' |
48 | import { isOutdated, throwIfNotValid } from '../utils' | 56 | import { isOutdated, throwIfNotValid } from '../utils' |
49 | import { VideoModel } from '../video/video' | 57 | import { VideoModel } from '../video/video' |
@@ -73,7 +81,8 @@ export const unusedActorAttributesForAPI = [ | |||
73 | required: false | 81 | required: false |
74 | }, | 82 | }, |
75 | { | 83 | { |
76 | model: AvatarModel, | 84 | model: ActorImageModel, |
85 | as: 'Avatar', | ||
77 | required: false | 86 | required: false |
78 | } | 87 | } |
79 | ] | 88 | ] |
@@ -100,7 +109,13 @@ export const unusedActorAttributesForAPI = [ | |||
100 | required: false | 109 | required: false |
101 | }, | 110 | }, |
102 | { | 111 | { |
103 | model: AvatarModel, | 112 | model: ActorImageModel, |
113 | as: 'Avatar', | ||
114 | required: false | ||
115 | }, | ||
116 | { | ||
117 | model: ActorImageModel, | ||
118 | as: 'Banner', | ||
104 | required: false | 119 | required: false |
105 | } | 120 | } |
106 | ] | 121 | ] |
@@ -213,18 +228,35 @@ export class ActorModel extends Model { | |||
213 | @UpdatedAt | 228 | @UpdatedAt |
214 | updatedAt: Date | 229 | updatedAt: Date |
215 | 230 | ||
216 | @ForeignKey(() => AvatarModel) | 231 | @ForeignKey(() => ActorImageModel) |
217 | @Column | 232 | @Column |
218 | avatarId: number | 233 | avatarId: number |
219 | 234 | ||
220 | @BelongsTo(() => AvatarModel, { | 235 | @ForeignKey(() => ActorImageModel) |
236 | @Column | ||
237 | bannerId: number | ||
238 | |||
239 | @BelongsTo(() => ActorImageModel, { | ||
221 | foreignKey: { | 240 | foreignKey: { |
241 | name: 'avatarId', | ||
222 | allowNull: true | 242 | allowNull: true |
223 | }, | 243 | }, |
244 | as: 'Avatar', | ||
224 | onDelete: 'set null', | 245 | onDelete: 'set null', |
225 | hooks: true | 246 | hooks: true |
226 | }) | 247 | }) |
227 | Avatar: AvatarModel | 248 | Avatar: ActorImageModel |
249 | |||
250 | @BelongsTo(() => ActorImageModel, { | ||
251 | foreignKey: { | ||
252 | name: 'bannerId', | ||
253 | allowNull: true | ||
254 | }, | ||
255 | as: 'Banner', | ||
256 | onDelete: 'set null', | ||
257 | hooks: true | ||
258 | }) | ||
259 | Banner: ActorImageModel | ||
228 | 260 | ||
229 | @HasMany(() => ActorFollowModel, { | 261 | @HasMany(() => ActorFollowModel, { |
230 | foreignKey: { | 262 | foreignKey: { |
@@ -496,7 +528,7 @@ export class ActorModel extends Model { | |||
496 | } | 528 | } |
497 | 529 | ||
498 | toFormattedSummaryJSON (this: MActorSummaryFormattable) { | 530 | toFormattedSummaryJSON (this: MActorSummaryFormattable) { |
499 | let avatar: Avatar = null | 531 | let avatar: ActorImage = null |
500 | if (this.Avatar) { | 532 | if (this.Avatar) { |
501 | avatar = this.Avatar.toFormattedJSON() | 533 | avatar = this.Avatar.toFormattedJSON() |
502 | } | 534 | } |
@@ -512,29 +544,51 @@ export class ActorModel extends Model { | |||
512 | toFormattedJSON (this: MActorFormattable) { | 544 | toFormattedJSON (this: MActorFormattable) { |
513 | const base = this.toFormattedSummaryJSON() | 545 | const base = this.toFormattedSummaryJSON() |
514 | 546 | ||
547 | let banner: ActorImage = null | ||
548 | if (this.Banner) { | ||
549 | banner = this.Banner.toFormattedJSON() | ||
550 | } | ||
551 | |||
515 | return Object.assign(base, { | 552 | return Object.assign(base, { |
516 | id: this.id, | 553 | id: this.id, |
517 | hostRedundancyAllowed: this.getRedundancyAllowed(), | 554 | hostRedundancyAllowed: this.getRedundancyAllowed(), |
518 | followingCount: this.followingCount, | 555 | followingCount: this.followingCount, |
519 | followersCount: this.followersCount, | 556 | followersCount: this.followersCount, |
557 | banner, | ||
520 | createdAt: this.createdAt, | 558 | createdAt: this.createdAt, |
521 | updatedAt: this.updatedAt | 559 | updatedAt: this.updatedAt |
522 | }) | 560 | }) |
523 | } | 561 | } |
524 | 562 | ||
525 | toActivityPubObject (this: MActorAP, name: string) { | 563 | toActivityPubObject (this: MActorAPChannel | MActorAPAccount, name: string) { |
526 | let icon: ActivityIconObject | 564 | let icon: ActivityIconObject |
565 | let image: ActivityIconObject | ||
527 | 566 | ||
528 | if (this.avatarId) { | 567 | if (this.avatarId) { |
529 | const extension = extname(this.Avatar.filename) | 568 | const extension = extname(this.Avatar.filename) |
530 | 569 | ||
531 | icon = { | 570 | icon = { |
532 | type: 'Image', | 571 | type: 'Image', |
533 | mediaType: extension === '.png' ? 'image/png' : 'image/jpeg', | 572 | mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension], |
573 | height: this.Avatar.height, | ||
574 | width: this.Avatar.width, | ||
534 | url: this.getAvatarUrl() | 575 | url: this.getAvatarUrl() |
535 | } | 576 | } |
536 | } | 577 | } |
537 | 578 | ||
579 | if (this.bannerId) { | ||
580 | const banner = (this as MActorAPChannel).Banner | ||
581 | const extension = extname(banner.filename) | ||
582 | |||
583 | image = { | ||
584 | type: 'Image', | ||
585 | mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension], | ||
586 | height: banner.height, | ||
587 | width: banner.width, | ||
588 | url: this.getBannerUrl() | ||
589 | } | ||
590 | } | ||
591 | |||
538 | const json = { | 592 | const json = { |
539 | type: this.type, | 593 | type: this.type, |
540 | id: this.url, | 594 | id: this.url, |
@@ -554,7 +608,8 @@ export class ActorModel extends Model { | |||
554 | owner: this.url, | 608 | owner: this.url, |
555 | publicKeyPem: this.publicKey | 609 | publicKeyPem: this.publicKey |
556 | }, | 610 | }, |
557 | icon | 611 | icon, |
612 | image | ||
558 | } | 613 | } |
559 | 614 | ||
560 | return activityPubContextify(json) | 615 | return activityPubContextify(json) |
@@ -624,6 +679,12 @@ export class ActorModel extends Model { | |||
624 | return WEBSERVER.URL + this.Avatar.getStaticPath() | 679 | return WEBSERVER.URL + this.Avatar.getStaticPath() |
625 | } | 680 | } |
626 | 681 | ||
682 | getBannerUrl () { | ||
683 | if (!this.bannerId) return undefined | ||
684 | |||
685 | return WEBSERVER.URL + this.Banner.getStaticPath() | ||
686 | } | ||
687 | |||
627 | isOutdated () { | 688 | isOutdated () { |
628 | if (this.isOwned()) return false | 689 | if (this.isOwned()) return false |
629 | 690 | ||
diff --git a/server/models/application/application.ts b/server/models/application/application.ts index 909569de1..21f8b1cbc 100644 --- a/server/models/application/application.ts +++ b/server/models/application/application.ts | |||
@@ -32,6 +32,10 @@ export class ApplicationModel extends Model { | |||
32 | @Column | 32 | @Column |
33 | migrationVersion: number | 33 | migrationVersion: number |
34 | 34 | ||
35 | @AllowNull(true) | ||
36 | @Column | ||
37 | latestPeerTubeVersion: string | ||
38 | |||
35 | @HasOne(() => AccountModel, { | 39 | @HasOne(() => AccountModel, { |
36 | foreignKey: { | 40 | foreignKey: { |
37 | allowNull: true | 41 | allowNull: true |
diff --git a/server/models/avatar/avatar.ts b/server/models/avatar/avatar.ts deleted file mode 100644 index 0d246a144..000000000 --- a/server/models/avatar/avatar.ts +++ /dev/null | |||
@@ -1,81 +0,0 @@ | |||
1 | import { join } from 'path' | ||
2 | import { AfterDestroy, AllowNull, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
3 | import { Avatar } from '../../../shared/models/avatars/avatar.model' | ||
4 | import { LAZY_STATIC_PATHS } from '../../initializers/constants' | ||
5 | import { logger } from '../../helpers/logger' | ||
6 | import { remove } from 'fs-extra' | ||
7 | import { CONFIG } from '../../initializers/config' | ||
8 | import { throwIfNotValid } from '../utils' | ||
9 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | ||
10 | import { MAvatarFormattable } from '@server/types/models' | ||
11 | |||
12 | @Table({ | ||
13 | tableName: 'avatar', | ||
14 | indexes: [ | ||
15 | { | ||
16 | fields: [ 'filename' ], | ||
17 | unique: true | ||
18 | } | ||
19 | ] | ||
20 | }) | ||
21 | export class AvatarModel extends Model { | ||
22 | |||
23 | @AllowNull(false) | ||
24 | @Column | ||
25 | filename: string | ||
26 | |||
27 | @AllowNull(true) | ||
28 | @Is('AvatarFileUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'fileUrl', true)) | ||
29 | @Column | ||
30 | fileUrl: string | ||
31 | |||
32 | @AllowNull(false) | ||
33 | @Column | ||
34 | onDisk: boolean | ||
35 | |||
36 | @CreatedAt | ||
37 | createdAt: Date | ||
38 | |||
39 | @UpdatedAt | ||
40 | updatedAt: Date | ||
41 | |||
42 | @AfterDestroy | ||
43 | static removeFilesAndSendDelete (instance: AvatarModel) { | ||
44 | logger.info('Removing avatar file %s.', instance.filename) | ||
45 | |||
46 | // Don't block the transaction | ||
47 | instance.removeAvatar() | ||
48 | .catch(err => logger.error('Cannot remove avatar file %s.', instance.filename, err)) | ||
49 | } | ||
50 | |||
51 | static loadByName (filename: string) { | ||
52 | const query = { | ||
53 | where: { | ||
54 | filename | ||
55 | } | ||
56 | } | ||
57 | |||
58 | return AvatarModel.findOne(query) | ||
59 | } | ||
60 | |||
61 | toFormattedJSON (this: MAvatarFormattable): Avatar { | ||
62 | return { | ||
63 | path: this.getStaticPath(), | ||
64 | createdAt: this.createdAt, | ||
65 | updatedAt: this.updatedAt | ||
66 | } | ||
67 | } | ||
68 | |||
69 | getStaticPath () { | ||
70 | return join(LAZY_STATIC_PATHS.AVATARS, this.filename) | ||
71 | } | ||
72 | |||
73 | getPath () { | ||
74 | return join(CONFIG.STORAGE.AVATARS_DIR, this.filename) | ||
75 | } | ||
76 | |||
77 | removeAvatar () { | ||
78 | const avatarPath = join(CONFIG.STORAGE.AVATARS_DIR, this.filename) | ||
79 | return remove(avatarPath) | ||
80 | } | ||
81 | } | ||
diff --git a/server/models/oauth/oauth-token.ts b/server/models/oauth/oauth-token.ts index 6bc6cf27c..27e643aa7 100644 --- a/server/models/oauth/oauth-token.ts +++ b/server/models/oauth/oauth-token.ts | |||
@@ -12,9 +12,10 @@ import { | |||
12 | Table, | 12 | Table, |
13 | UpdatedAt | 13 | UpdatedAt |
14 | } from 'sequelize-typescript' | 14 | } from 'sequelize-typescript' |
15 | import { TokensCache } from '@server/lib/auth/tokens-cache' | ||
16 | import { MUserAccountId } from '@server/types/models' | ||
15 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' | 17 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' |
16 | import { logger } from '../../helpers/logger' | 18 | import { logger } from '../../helpers/logger' |
17 | import { clearCacheByToken } from '../../lib/oauth-model' | ||
18 | import { AccountModel } from '../account/account' | 19 | import { AccountModel } from '../account/account' |
19 | import { UserModel } from '../account/user' | 20 | import { UserModel } from '../account/user' |
20 | import { ActorModel } from '../activitypub/actor' | 21 | import { ActorModel } from '../activitypub/actor' |
@@ -26,9 +27,7 @@ export type OAuthTokenInfo = { | |||
26 | client: { | 27 | client: { |
27 | id: number | 28 | id: number |
28 | } | 29 | } |
29 | user: { | 30 | user: MUserAccountId |
30 | id: number | ||
31 | } | ||
32 | token: MOAuthTokenUser | 31 | token: MOAuthTokenUser |
33 | } | 32 | } |
34 | 33 | ||
@@ -133,7 +132,7 @@ export class OAuthTokenModel extends Model { | |||
133 | @AfterUpdate | 132 | @AfterUpdate |
134 | @AfterDestroy | 133 | @AfterDestroy |
135 | static removeTokenCache (token: OAuthTokenModel) { | 134 | static removeTokenCache (token: OAuthTokenModel) { |
136 | return clearCacheByToken(token.accessToken) | 135 | return TokensCache.Instance.clearCacheByToken(token.accessToken) |
137 | } | 136 | } |
138 | 137 | ||
139 | static loadByRefreshToken (refreshToken: string) { | 138 | static loadByRefreshToken (refreshToken: string) { |
@@ -206,6 +205,8 @@ export class OAuthTokenModel extends Model { | |||
206 | } | 205 | } |
207 | 206 | ||
208 | static deleteUserToken (userId: number, t?: Transaction) { | 207 | static deleteUserToken (userId: number, t?: Transaction) { |
208 | TokensCache.Instance.deleteUserToken(userId) | ||
209 | |||
209 | const query = { | 210 | const query = { |
210 | where: { | 211 | where: { |
211 | userId | 212 | userId |
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index 53293df37..53ebadeaf 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts | |||
@@ -32,6 +32,7 @@ import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants' | |||
32 | import { ActorModel } from '../activitypub/actor' | 32 | import { ActorModel } from '../activitypub/actor' |
33 | import { ServerModel } from '../server/server' | 33 | import { ServerModel } from '../server/server' |
34 | import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils' | 34 | import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils' |
35 | import { ScheduleVideoUpdateModel } from '../video/schedule-video-update' | ||
35 | import { VideoModel } from '../video/video' | 36 | import { VideoModel } from '../video/video' |
36 | import { VideoChannelModel } from '../video/video-channel' | 37 | import { VideoChannelModel } from '../video/video-channel' |
37 | import { VideoFileModel } from '../video/video-file' | 38 | import { VideoFileModel } from '../video/video-file' |
@@ -374,7 +375,13 @@ export class VideoRedundancyModel extends Model { | |||
374 | ...this.buildVideoIdsForDuplication(peertubeActor) | 375 | ...this.buildVideoIdsForDuplication(peertubeActor) |
375 | }, | 376 | }, |
376 | include: [ | 377 | include: [ |
377 | VideoRedundancyModel.buildServerRedundancyInclude() | 378 | VideoRedundancyModel.buildServerRedundancyInclude(), |
379 | |||
380 | // Required by publishedAt sort | ||
381 | { | ||
382 | model: ScheduleVideoUpdateModel.unscoped(), | ||
383 | required: false | ||
384 | } | ||
378 | ] | 385 | ] |
379 | } | 386 | } |
380 | 387 | ||
diff --git a/server/models/utils.ts b/server/models/utils.ts index 5337ae75d..ec51c66bf 100644 --- a/server/models/utils.ts +++ b/server/models/utils.ts | |||
@@ -56,6 +56,14 @@ function getVideoSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): Or | |||
56 | 56 | ||
57 | lastSort | 57 | lastSort |
58 | ] | 58 | ] |
59 | } else if (field === 'publishedAt') { | ||
60 | return [ | ||
61 | [ 'ScheduleVideoUpdate', 'updateAt', direction + ' NULLS LAST' ], | ||
62 | |||
63 | [ Sequelize.col('VideoModel.publishedAt'), direction ], | ||
64 | |||
65 | lastSort | ||
66 | ] | ||
59 | } | 67 | } |
60 | 68 | ||
61 | let finalField: string | Col | 69 | let finalField: string | Col |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 178878c55..b7ffbd3b1 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { FindOptions, Includeable, literal, Op, ScopeOptions } from 'sequelize' | 1 | import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions } from 'sequelize' |
2 | import { | 2 | import { |
3 | AllowNull, | 3 | AllowNull, |
4 | BeforeDestroy, | 4 | BeforeDestroy, |
@@ -28,17 +28,16 @@ import { | |||
28 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' | 28 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' |
29 | import { sendDeleteActor } from '../../lib/activitypub/send' | 29 | import { sendDeleteActor } from '../../lib/activitypub/send' |
30 | import { | 30 | import { |
31 | MChannelAccountDefault, | ||
32 | MChannelActor, | 31 | MChannelActor, |
33 | MChannelActorAccountDefaultVideos, | ||
34 | MChannelAP, | 32 | MChannelAP, |
33 | MChannelBannerAccountDefault, | ||
35 | MChannelFormattable, | 34 | MChannelFormattable, |
36 | MChannelSummaryFormattable | 35 | MChannelSummaryFormattable |
37 | } from '../../types/models/video' | 36 | } from '../../types/models/video' |
38 | import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' | 37 | import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' |
38 | import { ActorImageModel } from '../account/actor-image' | ||
39 | import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' | 39 | import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' |
40 | import { ActorFollowModel } from '../activitypub/actor-follow' | 40 | import { ActorFollowModel } from '../activitypub/actor-follow' |
41 | import { AvatarModel } from '../avatar/avatar' | ||
42 | import { ServerModel } from '../server/server' | 41 | import { ServerModel } from '../server/server' |
43 | import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' | 42 | import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' |
44 | import { VideoModel } from './video' | 43 | import { VideoModel } from './video' |
@@ -49,6 +48,7 @@ export enum ScopeNames { | |||
49 | SUMMARY = 'SUMMARY', | 48 | SUMMARY = 'SUMMARY', |
50 | WITH_ACCOUNT = 'WITH_ACCOUNT', | 49 | WITH_ACCOUNT = 'WITH_ACCOUNT', |
51 | WITH_ACTOR = 'WITH_ACTOR', | 50 | WITH_ACTOR = 'WITH_ACTOR', |
51 | WITH_ACTOR_BANNER = 'WITH_ACTOR_BANNER', | ||
52 | WITH_VIDEOS = 'WITH_VIDEOS', | 52 | WITH_VIDEOS = 'WITH_VIDEOS', |
53 | WITH_STATS = 'WITH_STATS' | 53 | WITH_STATS = 'WITH_STATS' |
54 | } | 54 | } |
@@ -99,7 +99,14 @@ export type SummaryOptions = { | |||
99 | } | 99 | } |
100 | } | 100 | } |
101 | ] | 101 | ] |
102 | } | 102 | }, |
103 | include: [ | ||
104 | { | ||
105 | model: ActorImageModel, | ||
106 | as: 'Banner', | ||
107 | required: false | ||
108 | } | ||
109 | ] | ||
103 | }, | 110 | }, |
104 | { | 111 | { |
105 | model: AccountModel, | 112 | model: AccountModel, |
@@ -130,7 +137,8 @@ export type SummaryOptions = { | |||
130 | required: false | 137 | required: false |
131 | }, | 138 | }, |
132 | { | 139 | { |
133 | model: AvatarModel.unscoped(), | 140 | model: ActorImageModel.unscoped(), |
141 | as: 'Avatar', | ||
134 | required: false | 142 | required: false |
135 | } | 143 | } |
136 | ] | 144 | ] |
@@ -167,6 +175,20 @@ export type SummaryOptions = { | |||
167 | ActorModel | 175 | ActorModel |
168 | ] | 176 | ] |
169 | }, | 177 | }, |
178 | [ScopeNames.WITH_ACTOR_BANNER]: { | ||
179 | include: [ | ||
180 | { | ||
181 | model: ActorModel, | ||
182 | include: [ | ||
183 | { | ||
184 | model: ActorImageModel, | ||
185 | required: false, | ||
186 | as: 'Banner' | ||
187 | } | ||
188 | ] | ||
189 | } | ||
190 | ] | ||
191 | }, | ||
170 | [ScopeNames.WITH_VIDEOS]: { | 192 | [ScopeNames.WITH_VIDEOS]: { |
171 | include: [ | 193 | include: [ |
172 | VideoModel | 194 | VideoModel |
@@ -316,6 +338,47 @@ export class VideoChannelModel extends Model { | |||
316 | return VideoChannelModel.count(query) | 338 | return VideoChannelModel.count(query) |
317 | } | 339 | } |
318 | 340 | ||
341 | static async getStats () { | ||
342 | |||
343 | function getActiveVideoChannels (days: number) { | ||
344 | const options = { | ||
345 | type: QueryTypes.SELECT as QueryTypes.SELECT, | ||
346 | raw: true | ||
347 | } | ||
348 | |||
349 | const query = ` | ||
350 | SELECT COUNT(DISTINCT("VideoChannelModel"."id")) AS "count" | ||
351 | FROM "videoChannel" AS "VideoChannelModel" | ||
352 | INNER JOIN "video" AS "Videos" | ||
353 | ON "VideoChannelModel"."id" = "Videos"."channelId" | ||
354 | AND ("Videos"."publishedAt" > Now() - interval '${days}d') | ||
355 | INNER JOIN "account" AS "Account" | ||
356 | ON "VideoChannelModel"."accountId" = "Account"."id" | ||
357 | INNER JOIN "actor" AS "Account->Actor" | ||
358 | ON "Account"."actorId" = "Account->Actor"."id" | ||
359 | AND "Account->Actor"."serverId" IS NULL | ||
360 | LEFT OUTER JOIN "server" AS "Account->Actor->Server" | ||
361 | ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | ||
362 | |||
363 | return VideoChannelModel.sequelize.query<{ count: string }>(query, options) | ||
364 | .then(r => parseInt(r[0].count, 10)) | ||
365 | } | ||
366 | |||
367 | const totalLocalVideoChannels = await VideoChannelModel.count() | ||
368 | const totalLocalDailyActiveVideoChannels = await getActiveVideoChannels(1) | ||
369 | const totalLocalWeeklyActiveVideoChannels = await getActiveVideoChannels(7) | ||
370 | const totalLocalMonthlyActiveVideoChannels = await getActiveVideoChannels(30) | ||
371 | const totalHalfYearActiveVideoChannels = await getActiveVideoChannels(180) | ||
372 | |||
373 | return { | ||
374 | totalLocalVideoChannels, | ||
375 | totalLocalDailyActiveVideoChannels, | ||
376 | totalLocalWeeklyActiveVideoChannels, | ||
377 | totalLocalMonthlyActiveVideoChannels, | ||
378 | totalHalfYearActiveVideoChannels | ||
379 | } | ||
380 | } | ||
381 | |||
319 | static listForApi (parameters: { | 382 | static listForApi (parameters: { |
320 | actorId: number | 383 | actorId: number |
321 | start: number | 384 | start: number |
@@ -441,7 +504,7 @@ export class VideoChannelModel extends Model { | |||
441 | where | 504 | where |
442 | } | 505 | } |
443 | 506 | ||
444 | const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR ] | 507 | const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR_BANNER ] |
445 | 508 | ||
446 | if (options.withStats === true) { | 509 | if (options.withStats === true) { |
447 | scopes.push({ | 510 | scopes.push({ |
@@ -457,32 +520,13 @@ export class VideoChannelModel extends Model { | |||
457 | }) | 520 | }) |
458 | } | 521 | } |
459 | 522 | ||
460 | static loadByIdAndPopulateAccount (id: number): Promise<MChannelAccountDefault> { | 523 | static loadAndPopulateAccount (id: number): Promise<MChannelBannerAccountDefault> { |
461 | return VideoChannelModel.unscoped() | 524 | return VideoChannelModel.unscoped() |
462 | .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) | 525 | .scope([ ScopeNames.WITH_ACTOR_BANNER, ScopeNames.WITH_ACCOUNT ]) |
463 | .findByPk(id) | 526 | .findByPk(id) |
464 | } | 527 | } |
465 | 528 | ||
466 | static loadByIdAndAccount (id: number, accountId: number): Promise<MChannelAccountDefault> { | 529 | static loadByUrlAndPopulateAccount (url: string): Promise<MChannelBannerAccountDefault> { |
467 | const query = { | ||
468 | where: { | ||
469 | id, | ||
470 | accountId | ||
471 | } | ||
472 | } | ||
473 | |||
474 | return VideoChannelModel.unscoped() | ||
475 | .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) | ||
476 | .findOne(query) | ||
477 | } | ||
478 | |||
479 | static loadAndPopulateAccount (id: number): Promise<MChannelAccountDefault> { | ||
480 | return VideoChannelModel.unscoped() | ||
481 | .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) | ||
482 | .findByPk(id) | ||
483 | } | ||
484 | |||
485 | static loadByUrlAndPopulateAccount (url: string): Promise<MChannelAccountDefault> { | ||
486 | const query = { | 530 | const query = { |
487 | include: [ | 531 | include: [ |
488 | { | 532 | { |
@@ -490,7 +534,14 @@ export class VideoChannelModel extends Model { | |||
490 | required: true, | 534 | required: true, |
491 | where: { | 535 | where: { |
492 | url | 536 | url |
493 | } | 537 | }, |
538 | include: [ | ||
539 | { | ||
540 | model: ActorImageModel, | ||
541 | required: false, | ||
542 | as: 'Banner' | ||
543 | } | ||
544 | ] | ||
494 | } | 545 | } |
495 | ] | 546 | ] |
496 | } | 547 | } |
@@ -508,7 +559,7 @@ export class VideoChannelModel extends Model { | |||
508 | return VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host) | 559 | return VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host) |
509 | } | 560 | } |
510 | 561 | ||
511 | static loadLocalByNameAndPopulateAccount (name: string): Promise<MChannelAccountDefault> { | 562 | static loadLocalByNameAndPopulateAccount (name: string): Promise<MChannelBannerAccountDefault> { |
512 | const query = { | 563 | const query = { |
513 | include: [ | 564 | include: [ |
514 | { | 565 | { |
@@ -517,17 +568,24 @@ export class VideoChannelModel extends Model { | |||
517 | where: { | 568 | where: { |
518 | preferredUsername: name, | 569 | preferredUsername: name, |
519 | serverId: null | 570 | serverId: null |
520 | } | 571 | }, |
572 | include: [ | ||
573 | { | ||
574 | model: ActorImageModel, | ||
575 | required: false, | ||
576 | as: 'Banner' | ||
577 | } | ||
578 | ] | ||
521 | } | 579 | } |
522 | ] | 580 | ] |
523 | } | 581 | } |
524 | 582 | ||
525 | return VideoChannelModel.unscoped() | 583 | return VideoChannelModel.unscoped() |
526 | .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) | 584 | .scope([ ScopeNames.WITH_ACCOUNT ]) |
527 | .findOne(query) | 585 | .findOne(query) |
528 | } | 586 | } |
529 | 587 | ||
530 | static loadByNameAndHostAndPopulateAccount (name: string, host: string): Promise<MChannelAccountDefault> { | 588 | static loadByNameAndHostAndPopulateAccount (name: string, host: string): Promise<MChannelBannerAccountDefault> { |
531 | const query = { | 589 | const query = { |
532 | include: [ | 590 | include: [ |
533 | { | 591 | { |
@@ -541,6 +599,11 @@ export class VideoChannelModel extends Model { | |||
541 | model: ServerModel, | 599 | model: ServerModel, |
542 | required: true, | 600 | required: true, |
543 | where: { host } | 601 | where: { host } |
602 | }, | ||
603 | { | ||
604 | model: ActorImageModel, | ||
605 | required: false, | ||
606 | as: 'Banner' | ||
544 | } | 607 | } |
545 | ] | 608 | ] |
546 | } | 609 | } |
@@ -548,22 +611,10 @@ export class VideoChannelModel extends Model { | |||
548 | } | 611 | } |
549 | 612 | ||
550 | return VideoChannelModel.unscoped() | 613 | return VideoChannelModel.unscoped() |
551 | .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) | 614 | .scope([ ScopeNames.WITH_ACCOUNT ]) |
552 | .findOne(query) | 615 | .findOne(query) |
553 | } | 616 | } |
554 | 617 | ||
555 | static loadAndPopulateAccountAndVideos (id: number): Promise<MChannelActorAccountDefaultVideos> { | ||
556 | const options = { | ||
557 | include: [ | ||
558 | VideoModel | ||
559 | ] | ||
560 | } | ||
561 | |||
562 | return VideoChannelModel.unscoped() | ||
563 | .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_VIDEOS ]) | ||
564 | .findByPk(id, options) | ||
565 | } | ||
566 | |||
567 | toFormattedSummaryJSON (this: MChannelSummaryFormattable): VideoChannelSummary { | 618 | toFormattedSummaryJSON (this: MChannelSummaryFormattable): VideoChannelSummary { |
568 | const actor = this.Actor.toFormattedSummaryJSON() | 619 | const actor = this.Actor.toFormattedSummaryJSON() |
569 | 620 | ||
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index 49a406608..efe5be36d 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts | |||
@@ -54,6 +54,7 @@ import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getPlaylistSort, isOutdat | |||
54 | import { ThumbnailModel } from './thumbnail' | 54 | import { ThumbnailModel } from './thumbnail' |
55 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' | 55 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' |
56 | import { VideoPlaylistElementModel } from './video-playlist-element' | 56 | import { VideoPlaylistElementModel } from './video-playlist-element' |
57 | import { ActorModel } from '../activitypub/actor' | ||
57 | 58 | ||
58 | enum ScopeNames { | 59 | enum ScopeNames { |
59 | AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', | 60 | AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', |
@@ -65,7 +66,7 @@ enum ScopeNames { | |||
65 | } | 66 | } |
66 | 67 | ||
67 | type AvailableForListOptions = { | 68 | type AvailableForListOptions = { |
68 | followerActorId: number | 69 | followerActorId?: number |
69 | type?: VideoPlaylistType | 70 | type?: VideoPlaylistType |
70 | accountId?: number | 71 | accountId?: number |
71 | videoChannelId?: number | 72 | videoChannelId?: number |
@@ -134,20 +135,26 @@ type AvailableForListOptions = { | |||
134 | privacy: VideoPlaylistPrivacy.PUBLIC | 135 | privacy: VideoPlaylistPrivacy.PUBLIC |
135 | }) | 136 | }) |
136 | 137 | ||
137 | // Only list local playlists OR playlists that are on an instance followed by actorId | 138 | // Only list local playlists |
138 | const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId) | 139 | const whereActorOr: WhereOptions[] = [ |
140 | { | ||
141 | serverId: null | ||
142 | } | ||
143 | ] | ||
139 | 144 | ||
140 | whereActor = { | 145 | // … OR playlists that are on an instance followed by actorId |
141 | [Op.or]: [ | 146 | if (options.followerActorId) { |
142 | { | 147 | const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId) |
143 | serverId: null | 148 | |
144 | }, | 149 | whereActorOr.push({ |
145 | { | 150 | serverId: { |
146 | serverId: { | 151 | [Op.in]: literal(inQueryInstanceFollow) |
147 | [Op.in]: literal(inQueryInstanceFollow) | ||
148 | } | ||
149 | } | 152 | } |
150 | ] | 153 | }) |
154 | } | ||
155 | |||
156 | whereActor = { | ||
157 | [Op.or]: whereActorOr | ||
151 | } | 158 | } |
152 | } | 159 | } |
153 | 160 | ||
@@ -495,6 +502,33 @@ export class VideoPlaylistModel extends Model { | |||
495 | return '/video-playlists/embed/' + this.uuid | 502 | return '/video-playlists/embed/' + this.uuid |
496 | } | 503 | } |
497 | 504 | ||
505 | static async getStats () { | ||
506 | const totalLocalPlaylists = await VideoPlaylistModel.count({ | ||
507 | include: [ | ||
508 | { | ||
509 | model: AccountModel, | ||
510 | required: true, | ||
511 | include: [ | ||
512 | { | ||
513 | model: ActorModel, | ||
514 | required: true, | ||
515 | where: { | ||
516 | serverId: null | ||
517 | } | ||
518 | } | ||
519 | ] | ||
520 | } | ||
521 | ], | ||
522 | where: { | ||
523 | privacy: VideoPlaylistPrivacy.PUBLIC | ||
524 | } | ||
525 | }) | ||
526 | |||
527 | return { | ||
528 | totalLocalPlaylists | ||
529 | } | ||
530 | } | ||
531 | |||
498 | setAsRefreshed () { | 532 | setAsRefreshed () { |
499 | this.changed('updatedAt', true) | 533 | this.changed('updatedAt', true) |
500 | 534 | ||
diff --git a/server/models/video/video-query-builder.ts b/server/models/video/video-query-builder.ts index 96df0a7f8..4d95ddee2 100644 --- a/server/models/video/video-query-builder.ts +++ b/server/models/video/video-query-builder.ts | |||
@@ -490,12 +490,13 @@ function wrapForAPIResults (baseQuery: string, replacements: any, options: Build | |||
490 | 'INNER JOIN "actor" AS "VideoChannel->Account->Actor" ON "VideoChannel->Account"."actorId" = "VideoChannel->Account->Actor"."id"', | 490 | 'INNER JOIN "actor" AS "VideoChannel->Account->Actor" ON "VideoChannel->Account"."actorId" = "VideoChannel->Account->Actor"."id"', |
491 | 491 | ||
492 | 'LEFT OUTER JOIN "server" AS "VideoChannel->Actor->Server" ON "VideoChannel->Actor"."serverId" = "VideoChannel->Actor->Server"."id"', | 492 | 'LEFT OUTER JOIN "server" AS "VideoChannel->Actor->Server" ON "VideoChannel->Actor"."serverId" = "VideoChannel->Actor->Server"."id"', |
493 | 'LEFT OUTER JOIN "avatar" AS "VideoChannel->Actor->Avatar" ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"', | 493 | 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Actor->Avatar" ' + |
494 | 'ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"', | ||
494 | 495 | ||
495 | 'LEFT OUTER JOIN "server" AS "VideoChannel->Account->Actor->Server" ' + | 496 | 'LEFT OUTER JOIN "server" AS "VideoChannel->Account->Actor->Server" ' + |
496 | 'ON "VideoChannel->Account->Actor"."serverId" = "VideoChannel->Account->Actor->Server"."id"', | 497 | 'ON "VideoChannel->Account->Actor"."serverId" = "VideoChannel->Account->Actor->Server"."id"', |
497 | 498 | ||
498 | 'LEFT OUTER JOIN "avatar" AS "VideoChannel->Account->Actor->Avatar" ' + | 499 | 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Account->Actor->Avatar" ' + |
499 | 'ON "VideoChannel->Account->Actor"."avatarId" = "VideoChannel->Account->Actor->Avatar"."id"', | 500 | 'ON "VideoChannel->Account->Actor"."avatarId" = "VideoChannel->Account->Actor->Avatar"."id"', |
500 | 501 | ||
501 | 'LEFT OUTER JOIN "thumbnail" AS "Thumbnails" ON "video"."id" = "Thumbnails"."videoId"' | 502 | 'LEFT OUTER JOIN "thumbnail" AS "Thumbnails" ON "video"."id" = "Thumbnails"."videoId"' |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 3c4f3d3df..422bf6deb 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -24,7 +24,6 @@ import { | |||
24 | Table, | 24 | Table, |
25 | UpdatedAt | 25 | UpdatedAt |
26 | } from 'sequelize-typescript' | 26 | } from 'sequelize-typescript' |
27 | import { v4 as uuidv4 } from 'uuid' | ||
28 | import { buildNSFWFilter } from '@server/helpers/express-utils' | 27 | import { buildNSFWFilter } from '@server/helpers/express-utils' |
29 | import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' | 28 | import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' |
30 | import { LiveManager } from '@server/lib/live-manager' | 29 | import { LiveManager } from '@server/lib/live-manager' |
@@ -100,10 +99,10 @@ import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models | |||
100 | import { VideoAbuseModel } from '../abuse/video-abuse' | 99 | import { VideoAbuseModel } from '../abuse/video-abuse' |
101 | import { AccountModel } from '../account/account' | 100 | import { AccountModel } from '../account/account' |
102 | import { AccountVideoRateModel } from '../account/account-video-rate' | 101 | import { AccountVideoRateModel } from '../account/account-video-rate' |
102 | import { ActorImageModel } from '../account/actor-image' | ||
103 | import { UserModel } from '../account/user' | 103 | import { UserModel } from '../account/user' |
104 | import { UserVideoHistoryModel } from '../account/user-video-history' | 104 | import { UserVideoHistoryModel } from '../account/user-video-history' |
105 | import { ActorModel } from '../activitypub/actor' | 105 | import { ActorModel } from '../activitypub/actor' |
106 | import { AvatarModel } from '../avatar/avatar' | ||
107 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 106 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
108 | import { ServerModel } from '../server/server' | 107 | import { ServerModel } from '../server/server' |
109 | import { TrackerModel } from '../server/tracker' | 108 | import { TrackerModel } from '../server/tracker' |
@@ -286,7 +285,8 @@ export type AvailableForListIDsOptions = { | |||
286 | required: false | 285 | required: false |
287 | }, | 286 | }, |
288 | { | 287 | { |
289 | model: AvatarModel.unscoped(), | 288 | model: ActorImageModel.unscoped(), |
289 | as: 'Avatar', | ||
290 | required: false | 290 | required: false |
291 | } | 291 | } |
292 | ] | 292 | ] |
@@ -308,7 +308,8 @@ export type AvailableForListIDsOptions = { | |||
308 | required: false | 308 | required: false |
309 | }, | 309 | }, |
310 | { | 310 | { |
311 | model: AvatarModel.unscoped(), | 311 | model: ActorImageModel.unscoped(), |
312 | as: 'Avatar', | ||
312 | required: false | 313 | required: false |
313 | } | 314 | } |
314 | ] | 315 | ] |
@@ -1703,7 +1704,7 @@ export class VideoModel extends Model { | |||
1703 | 1704 | ||
1704 | function buildActor (rowActor: any) { | 1705 | function buildActor (rowActor: any) { |
1705 | const avatarModel = rowActor.Avatar.id !== null | 1706 | const avatarModel = rowActor.Avatar.id !== null |
1706 | ? new AvatarModel(pick(rowActor.Avatar, avatarKeys), buildOpts) | 1707 | ? new ActorImageModel(pick(rowActor.Avatar, avatarKeys), buildOpts) |
1707 | : null | 1708 | : null |
1708 | 1709 | ||
1709 | const serverModel = rowActor.Server.id !== null | 1710 | const serverModel = rowActor.Server.id !== null |
@@ -1869,20 +1870,12 @@ export class VideoModel extends Model { | |||
1869 | this.Thumbnails.push(savedThumbnail) | 1870 | this.Thumbnails.push(savedThumbnail) |
1870 | } | 1871 | } |
1871 | 1872 | ||
1872 | generateThumbnailName () { | ||
1873 | return uuidv4() + '.jpg' | ||
1874 | } | ||
1875 | |||
1876 | getMiniature () { | 1873 | getMiniature () { |
1877 | if (Array.isArray(this.Thumbnails) === false) return undefined | 1874 | if (Array.isArray(this.Thumbnails) === false) return undefined |
1878 | 1875 | ||
1879 | return this.Thumbnails.find(t => t.type === ThumbnailType.MINIATURE) | 1876 | return this.Thumbnails.find(t => t.type === ThumbnailType.MINIATURE) |
1880 | } | 1877 | } |
1881 | 1878 | ||
1882 | generatePreviewName () { | ||
1883 | return uuidv4() + '.jpg' | ||
1884 | } | ||
1885 | |||
1886 | hasPreview () { | 1879 | hasPreview () { |
1887 | return !!this.getPreview() | 1880 | return !!this.getPreview() |
1888 | } | 1881 | } |
@@ -2034,9 +2027,11 @@ export class VideoModel extends Model { | |||
2034 | } | 2027 | } |
2035 | 2028 | ||
2036 | setAsRefreshed () { | 2029 | setAsRefreshed () { |
2037 | this.changed('updatedAt', true) | 2030 | const options = { |
2031 | where: { id: this.id } | ||
2032 | } | ||
2038 | 2033 | ||
2039 | return this.save() | 2034 | return VideoModel.update({ updatedAt: new Date() }, options) |
2040 | } | 2035 | } |
2041 | 2036 | ||
2042 | requiresAuth () { | 2037 | requiresAuth () { |