diff options
Diffstat (limited to 'server/models')
53 files changed, 2317 insertions, 1182 deletions
diff --git a/server/models/abuse/abuse-message.ts b/server/models/abuse/abuse-message.ts index 7e51b3e07..2c5987e96 100644 --- a/server/models/abuse/abuse-message.ts +++ b/server/models/abuse/abuse-message.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { isAbuseMessageValid } from '@server/helpers/custom-validators/abuses' | 2 | import { isAbuseMessageValid } from '@server/helpers/custom-validators/abuses' |
3 | import { MAbuseMessage, MAbuseMessageFormattable } from '@server/types/models' | 3 | import { MAbuseMessage, MAbuseMessageFormattable } from '@server/types/models' |
4 | import { AttributesOnly } from '@shared/core-utils' | ||
4 | import { AbuseMessage } from '@shared/models' | 5 | import { AbuseMessage } from '@shared/models' |
5 | import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' | 6 | import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' |
6 | import { getSort, throwIfNotValid } from '../utils' | 7 | import { getSort, throwIfNotValid } from '../utils' |
@@ -17,7 +18,7 @@ import { AbuseModel } from './abuse' | |||
17 | } | 18 | } |
18 | ] | 19 | ] |
19 | }) | 20 | }) |
20 | export class AbuseMessageModel extends Model { | 21 | export class AbuseMessageModel extends Model<Partial<AttributesOnly<AbuseMessageModel>>> { |
21 | 22 | ||
22 | @AllowNull(false) | 23 | @AllowNull(false) |
23 | @Is('AbuseMessage', value => throwIfNotValid(value, isAbuseMessageValid, 'message')) | 24 | @Is('AbuseMessage', value => throwIfNotValid(value, isAbuseMessageValid, 'message')) |
diff --git a/server/models/abuse/abuse.ts b/server/models/abuse/abuse.ts index 262f364f1..3518f5c02 100644 --- a/server/models/abuse/abuse.ts +++ b/server/models/abuse/abuse.ts | |||
@@ -16,7 +16,7 @@ import { | |||
16 | UpdatedAt | 16 | UpdatedAt |
17 | } from 'sequelize-typescript' | 17 | } from 'sequelize-typescript' |
18 | import { isAbuseModerationCommentValid, isAbuseReasonValid, isAbuseStateValid } from '@server/helpers/custom-validators/abuses' | 18 | import { isAbuseModerationCommentValid, isAbuseReasonValid, isAbuseStateValid } from '@server/helpers/custom-validators/abuses' |
19 | import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse' | 19 | import { abusePredefinedReasonsMap, AttributesOnly } from '@shared/core-utils' |
20 | import { | 20 | import { |
21 | AbuseFilter, | 21 | AbuseFilter, |
22 | AbuseObject, | 22 | AbuseObject, |
@@ -187,7 +187,7 @@ export enum ScopeNames { | |||
187 | } | 187 | } |
188 | ] | 188 | ] |
189 | }) | 189 | }) |
190 | export class AbuseModel extends Model { | 190 | export class AbuseModel extends Model<Partial<AttributesOnly<AbuseModel>>> { |
191 | 191 | ||
192 | @AllowNull(false) | 192 | @AllowNull(false) |
193 | @Default(null) | 193 | @Default(null) |
diff --git a/server/models/abuse/video-abuse.ts b/server/models/abuse/video-abuse.ts index 90aa0695e..95bff50d0 100644 --- a/server/models/abuse/video-abuse.ts +++ b/server/models/abuse/video-abuse.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { AttributesOnly } from '@shared/core-utils' | ||
2 | import { VideoDetails } from '@shared/models' | 3 | import { VideoDetails } from '@shared/models' |
3 | import { VideoModel } from '../video/video' | 4 | import { VideoModel } from '../video/video' |
4 | import { AbuseModel } from './abuse' | 5 | import { AbuseModel } from './abuse' |
@@ -14,7 +15,7 @@ import { AbuseModel } from './abuse' | |||
14 | } | 15 | } |
15 | ] | 16 | ] |
16 | }) | 17 | }) |
17 | export class VideoAbuseModel extends Model { | 18 | export class VideoAbuseModel extends Model<Partial<AttributesOnly<VideoAbuseModel>>> { |
18 | 19 | ||
19 | @CreatedAt | 20 | @CreatedAt |
20 | createdAt: Date | 21 | createdAt: Date |
diff --git a/server/models/abuse/video-comment-abuse.ts b/server/models/abuse/video-comment-abuse.ts index d3fce76a5..32cb2ca64 100644 --- a/server/models/abuse/video-comment-abuse.ts +++ b/server/models/abuse/video-comment-abuse.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { AttributesOnly } from '@shared/core-utils' | ||
2 | import { VideoCommentModel } from '../video/video-comment' | 3 | import { VideoCommentModel } from '../video/video-comment' |
3 | import { AbuseModel } from './abuse' | 4 | import { AbuseModel } from './abuse' |
4 | 5 | ||
@@ -13,7 +14,7 @@ import { AbuseModel } from './abuse' | |||
13 | } | 14 | } |
14 | ] | 15 | ] |
15 | }) | 16 | }) |
16 | export class VideoCommentAbuseModel extends Model { | 17 | export class VideoCommentAbuseModel extends Model<Partial<AttributesOnly<VideoCommentAbuseModel>>> { |
17 | 18 | ||
18 | @CreatedAt | 19 | @CreatedAt |
19 | createdAt: Date | 20 | createdAt: Date |
diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts index fe9168ab8..b2375b006 100644 --- a/server/models/account/account-blocklist.ts +++ b/server/models/account/account-blocklist.ts | |||
@@ -1,8 +1,9 @@ | |||
1 | import { Op } from 'sequelize' | 1 | import { Op } from 'sequelize' |
2 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models' | 3 | import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models' |
4 | import { AttributesOnly } from '@shared/core-utils' | ||
4 | import { AccountBlock } from '../../../shared/models' | 5 | import { AccountBlock } from '../../../shared/models' |
5 | import { ActorModel } from '../activitypub/actor' | 6 | import { ActorModel } from '../actor/actor' |
6 | import { ServerModel } from '../server/server' | 7 | import { ServerModel } from '../server/server' |
7 | import { getSort, searchAttribute } from '../utils' | 8 | import { getSort, searchAttribute } from '../utils' |
8 | import { AccountModel } from './account' | 9 | import { AccountModel } from './account' |
@@ -40,7 +41,7 @@ enum ScopeNames { | |||
40 | } | 41 | } |
41 | ] | 42 | ] |
42 | }) | 43 | }) |
43 | export class AccountBlocklistModel extends Model { | 44 | export class AccountBlocklistModel extends Model<Partial<AttributesOnly<AccountBlocklistModel>>> { |
44 | 45 | ||
45 | @CreatedAt | 46 | @CreatedAt |
46 | createdAt: Date | 47 | createdAt: Date |
diff --git a/server/models/account/account-video-rate.ts b/server/models/account/account-video-rate.ts index 801f76bba..ee6dbc6da 100644 --- a/server/models/account/account-video-rate.ts +++ b/server/models/account/account-video-rate.ts | |||
@@ -7,11 +7,12 @@ import { | |||
7 | MAccountVideoRateAccountVideo, | 7 | MAccountVideoRateAccountVideo, |
8 | MAccountVideoRateFormattable | 8 | MAccountVideoRateFormattable |
9 | } from '@server/types/models/video/video-rate' | 9 | } from '@server/types/models/video/video-rate' |
10 | import { AttributesOnly } from '@shared/core-utils' | ||
10 | import { AccountVideoRate } from '../../../shared' | 11 | import { AccountVideoRate } from '../../../shared' |
11 | import { VideoRateType } from '../../../shared/models/videos' | 12 | import { VideoRateType } from '../../../shared/models/videos' |
12 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 13 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
13 | import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers/constants' | 14 | import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers/constants' |
14 | import { ActorModel } from '../activitypub/actor' | 15 | import { ActorModel } from '../actor/actor' |
15 | import { buildLocalAccountIdsIn, getSort, throwIfNotValid } from '../utils' | 16 | import { buildLocalAccountIdsIn, getSort, throwIfNotValid } from '../utils' |
16 | import { VideoModel } from '../video/video' | 17 | import { VideoModel } from '../video/video' |
17 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel' | 18 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel' |
@@ -42,7 +43,7 @@ import { AccountModel } from './account' | |||
42 | } | 43 | } |
43 | ] | 44 | ] |
44 | }) | 45 | }) |
45 | export class AccountVideoRateModel extends Model { | 46 | export class AccountVideoRateModel extends Model<Partial<AttributesOnly<AccountVideoRateModel>>> { |
46 | 47 | ||
47 | @AllowNull(false) | 48 | @AllowNull(false) |
48 | @Column(DataType.ENUM(...values(VIDEO_RATE_TYPES))) | 49 | @Column(DataType.ENUM(...values(VIDEO_RATE_TYPES))) |
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index d33353af7..665ecd595 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -17,10 +17,11 @@ import { | |||
17 | UpdatedAt | 17 | UpdatedAt |
18 | } from 'sequelize-typescript' | 18 | } from 'sequelize-typescript' |
19 | import { ModelCache } from '@server/models/model-cache' | 19 | import { ModelCache } from '@server/models/model-cache' |
20 | import { AttributesOnly } from '@shared/core-utils' | ||
20 | import { Account, AccountSummary } from '../../../shared/models/actors' | 21 | import { Account, AccountSummary } from '../../../shared/models/actors' |
21 | import { isAccountDescriptionValid } from '../../helpers/custom-validators/accounts' | 22 | import { isAccountDescriptionValid } from '../../helpers/custom-validators/accounts' |
22 | import { CONSTRAINTS_FIELDS, SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants' | 23 | import { CONSTRAINTS_FIELDS, SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants' |
23 | import { sendDeleteActor } from '../../lib/activitypub/send' | 24 | import { sendDeleteActor } from '../../lib/activitypub/send/send-delete' |
24 | import { | 25 | import { |
25 | MAccount, | 26 | MAccount, |
26 | MAccountActor, | 27 | MAccountActor, |
@@ -30,19 +31,19 @@ import { | |||
30 | MAccountSummaryFormattable, | 31 | MAccountSummaryFormattable, |
31 | MChannelActor | 32 | MChannelActor |
32 | } from '../../types/models' | 33 | } from '../../types/models' |
33 | import { ActorModel } from '../activitypub/actor' | 34 | import { ActorModel } from '../actor/actor' |
34 | import { ActorFollowModel } from '../activitypub/actor-follow' | 35 | import { ActorFollowModel } from '../actor/actor-follow' |
36 | import { ActorImageModel } from '../actor/actor-image' | ||
35 | import { ApplicationModel } from '../application/application' | 37 | import { ApplicationModel } from '../application/application' |
36 | import { ActorImageModel } from './actor-image' | ||
37 | import { ServerModel } from '../server/server' | 38 | import { ServerModel } from '../server/server' |
38 | import { ServerBlocklistModel } from '../server/server-blocklist' | 39 | import { ServerBlocklistModel } from '../server/server-blocklist' |
40 | import { UserModel } from '../user/user' | ||
39 | import { getSort, throwIfNotValid } from '../utils' | 41 | import { getSort, throwIfNotValid } from '../utils' |
40 | import { VideoModel } from '../video/video' | 42 | import { VideoModel } from '../video/video' |
41 | import { VideoChannelModel } from '../video/video-channel' | 43 | import { VideoChannelModel } from '../video/video-channel' |
42 | import { VideoCommentModel } from '../video/video-comment' | 44 | import { VideoCommentModel } from '../video/video-comment' |
43 | import { VideoPlaylistModel } from '../video/video-playlist' | 45 | import { VideoPlaylistModel } from '../video/video-playlist' |
44 | import { AccountBlocklistModel } from './account-blocklist' | 46 | import { AccountBlocklistModel } from './account-blocklist' |
45 | import { UserModel } from './user' | ||
46 | 47 | ||
47 | export enum ScopeNames { | 48 | export enum ScopeNames { |
48 | SUMMARY = 'SUMMARY' | 49 | SUMMARY = 'SUMMARY' |
@@ -141,7 +142,7 @@ export type SummaryOptions = { | |||
141 | } | 142 | } |
142 | ] | 143 | ] |
143 | }) | 144 | }) |
144 | export class AccountModel extends Model { | 145 | export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> { |
145 | 146 | ||
146 | @AllowNull(false) | 147 | @AllowNull(false) |
147 | @Column | 148 | @Column |
diff --git a/server/models/account/actor-custom-page.ts b/server/models/account/actor-custom-page.ts new file mode 100644 index 000000000..893023181 --- /dev/null +++ b/server/models/account/actor-custom-page.ts | |||
@@ -0,0 +1,69 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
2 | import { CustomPage } from '@shared/models' | ||
3 | import { ActorModel } from '../actor/actor' | ||
4 | import { getServerActor } from '../application/application' | ||
5 | |||
6 | @Table({ | ||
7 | tableName: 'actorCustomPage', | ||
8 | indexes: [ | ||
9 | { | ||
10 | fields: [ 'actorId', 'type' ], | ||
11 | unique: true | ||
12 | } | ||
13 | ] | ||
14 | }) | ||
15 | export class ActorCustomPageModel extends Model { | ||
16 | |||
17 | @AllowNull(true) | ||
18 | @Column(DataType.TEXT) | ||
19 | content: string | ||
20 | |||
21 | @AllowNull(false) | ||
22 | @Column | ||
23 | type: 'homepage' | ||
24 | |||
25 | @CreatedAt | ||
26 | createdAt: Date | ||
27 | |||
28 | @UpdatedAt | ||
29 | updatedAt: Date | ||
30 | |||
31 | @ForeignKey(() => ActorModel) | ||
32 | @Column | ||
33 | actorId: number | ||
34 | |||
35 | @BelongsTo(() => ActorModel, { | ||
36 | foreignKey: { | ||
37 | name: 'actorId', | ||
38 | allowNull: false | ||
39 | }, | ||
40 | onDelete: 'cascade' | ||
41 | }) | ||
42 | Actor: ActorModel | ||
43 | |||
44 | static async updateInstanceHomepage (content: string) { | ||
45 | const serverActor = await getServerActor() | ||
46 | |||
47 | return ActorCustomPageModel.upsert({ | ||
48 | content, | ||
49 | actorId: serverActor.id, | ||
50 | type: 'homepage' | ||
51 | }) | ||
52 | } | ||
53 | |||
54 | static async loadInstanceHomepage () { | ||
55 | const serverActor = await getServerActor() | ||
56 | |||
57 | return ActorCustomPageModel.findOne({ | ||
58 | where: { | ||
59 | actorId: serverActor.id | ||
60 | } | ||
61 | }) | ||
62 | } | ||
63 | |||
64 | toFormattedJSON (): CustomPage { | ||
65 | return { | ||
66 | content: this.content | ||
67 | } | ||
68 | } | ||
69 | } | ||
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/actor/actor-follow.ts index 4c5f37620..3a09e51d6 100644 --- a/server/models/activitypub/actor-follow.ts +++ b/server/models/actor/actor-follow.ts | |||
@@ -28,6 +28,7 @@ import { | |||
28 | MActorFollowFormattable, | 28 | MActorFollowFormattable, |
29 | MActorFollowSubscriptions | 29 | MActorFollowSubscriptions |
30 | } from '@server/types/models' | 30 | } from '@server/types/models' |
31 | import { AttributesOnly } from '@shared/core-utils' | ||
31 | import { ActivityPubActorType } from '@shared/models' | 32 | import { ActivityPubActorType } from '@shared/models' |
32 | import { FollowState } from '../../../shared/models/actors' | 33 | import { FollowState } from '../../../shared/models/actors' |
33 | import { ActorFollow } from '../../../shared/models/actors/follow.model' | 34 | import { ActorFollow } from '../../../shared/models/actors/follow.model' |
@@ -61,7 +62,7 @@ import { ActorModel, unusedActorAttributesForAPI } from './actor' | |||
61 | } | 62 | } |
62 | ] | 63 | ] |
63 | }) | 64 | }) |
64 | export class ActorFollowModel extends Model { | 65 | export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowModel>>> { |
65 | 66 | ||
66 | @AllowNull(false) | 67 | @AllowNull(false) |
67 | @Column(DataType.ENUM(...values(FOLLOW_STATES))) | 68 | @Column(DataType.ENUM(...values(FOLLOW_STATES))) |
@@ -619,7 +620,7 @@ export class ActorFollowModel extends Model { | |||
619 | if (serverIds.length === 0) return | 620 | if (serverIds.length === 0) return |
620 | 621 | ||
621 | const me = await getServerActor() | 622 | const me = await getServerActor() |
622 | const serverIdsString = createSafeIn(ActorFollowModel, serverIds) | 623 | const serverIdsString = createSafeIn(ActorFollowModel.sequelize, serverIds) |
623 | 624 | ||
624 | const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` + | 625 | const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` + |
625 | 'WHERE id IN (' + | 626 | 'WHERE id IN (' + |
diff --git a/server/models/account/actor-image.ts b/server/models/actor/actor-image.ts index ae05b4969..98a7f6fba 100644 --- a/server/models/account/actor-image.ts +++ b/server/models/actor/actor-image.ts | |||
@@ -2,6 +2,7 @@ import { remove } from 'fs-extra' | |||
2 | import { join } from 'path' | 2 | import { join } from 'path' |
3 | import { AfterDestroy, AllowNull, Column, CreatedAt, Default, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | 3 | import { AfterDestroy, AllowNull, Column, CreatedAt, Default, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
4 | import { MActorImageFormattable } from '@server/types/models' | 4 | import { MActorImageFormattable } from '@server/types/models' |
5 | import { AttributesOnly } from '@shared/core-utils' | ||
5 | import { ActorImageType } from '@shared/models' | 6 | import { ActorImageType } from '@shared/models' |
6 | import { ActorImage } from '../../../shared/models/actors/actor-image.model' | 7 | import { ActorImage } from '../../../shared/models/actors/actor-image.model' |
7 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 8 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
@@ -19,7 +20,7 @@ import { throwIfNotValid } from '../utils' | |||
19 | } | 20 | } |
20 | ] | 21 | ] |
21 | }) | 22 | }) |
22 | export class ActorImageModel extends Model { | 23 | export class ActorImageModel extends Model<Partial<AttributesOnly<ActorImageModel>>> { |
23 | 24 | ||
24 | @AllowNull(false) | 25 | @AllowNull(false) |
25 | @Column | 26 | @Column |
@@ -97,4 +98,8 @@ export class ActorImageModel extends Model { | |||
97 | const imagePath = join(CONFIG.STORAGE.ACTOR_IMAGES, this.filename) | 98 | const imagePath = join(CONFIG.STORAGE.ACTOR_IMAGES, this.filename) |
98 | return remove(imagePath) | 99 | return remove(imagePath) |
99 | } | 100 | } |
101 | |||
102 | isOwned () { | ||
103 | return !this.fileUrl | ||
104 | } | ||
100 | } | 105 | } |
diff --git a/server/models/activitypub/actor.ts b/server/models/actor/actor.ts index 1af9efac2..8df49951d 100644 --- a/server/models/activitypub/actor.ts +++ b/server/models/actor/actor.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import { values } from 'lodash' | 1 | import { values } from 'lodash' |
2 | import { extname } from 'path' | ||
3 | import { literal, Op, Transaction } from 'sequelize' | 2 | import { literal, Op, Transaction } from 'sequelize' |
4 | import { | 3 | import { |
5 | AllowNull, | 4 | AllowNull, |
@@ -17,7 +16,9 @@ import { | |||
17 | Table, | 16 | Table, |
18 | UpdatedAt | 17 | UpdatedAt |
19 | } from 'sequelize-typescript' | 18 | } from 'sequelize-typescript' |
19 | import { getLowercaseExtension } from '@server/helpers/core-utils' | ||
20 | import { ModelCache } from '@server/models/model-cache' | 20 | import { ModelCache } from '@server/models/model-cache' |
21 | import { AttributesOnly } from '@shared/core-utils' | ||
21 | import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub' | 22 | import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub' |
22 | import { ActorImage } from '../../../shared/models/actors/actor-image.model' | 23 | import { ActorImage } from '../../../shared/models/actors/actor-image.model' |
23 | import { activityPubContextify } from '../../helpers/activitypub' | 24 | import { activityPubContextify } from '../../helpers/activitypub' |
@@ -51,12 +52,12 @@ import { | |||
51 | MActorWithInboxes | 52 | MActorWithInboxes |
52 | } from '../../types/models' | 53 | } from '../../types/models' |
53 | import { AccountModel } from '../account/account' | 54 | import { AccountModel } from '../account/account' |
54 | import { ActorImageModel } from '../account/actor-image' | ||
55 | import { ServerModel } from '../server/server' | 55 | import { ServerModel } from '../server/server' |
56 | import { isOutdated, throwIfNotValid } from '../utils' | 56 | import { isOutdated, throwIfNotValid } from '../utils' |
57 | import { VideoModel } from '../video/video' | 57 | import { VideoModel } from '../video/video' |
58 | import { VideoChannelModel } from '../video/video-channel' | 58 | import { VideoChannelModel } from '../video/video-channel' |
59 | import { ActorFollowModel } from './actor-follow' | 59 | import { ActorFollowModel } from './actor-follow' |
60 | import { ActorImageModel } from './actor-image' | ||
60 | 61 | ||
61 | enum ScopeNames { | 62 | enum ScopeNames { |
62 | FULL = 'FULL' | 63 | FULL = 'FULL' |
@@ -159,7 +160,7 @@ export const unusedActorAttributesForAPI = [ | |||
159 | } | 160 | } |
160 | ] | 161 | ] |
161 | }) | 162 | }) |
162 | export class ActorModel extends Model { | 163 | export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { |
163 | 164 | ||
164 | @AllowNull(false) | 165 | @AllowNull(false) |
165 | @Column(DataType.ENUM(...values(ACTIVITY_PUB_ACTOR_TYPES))) | 166 | @Column(DataType.ENUM(...values(ACTIVITY_PUB_ACTOR_TYPES))) |
@@ -495,7 +496,7 @@ export class ActorModel extends Model { | |||
495 | }, { where, transaction }) | 496 | }, { where, transaction }) |
496 | } | 497 | } |
497 | 498 | ||
498 | static loadAccountActorByVideoId (videoId: number): Promise<MActor> { | 499 | static loadAccountActorByVideoId (videoId: number, transaction: Transaction): Promise<MActor> { |
499 | const query = { | 500 | const query = { |
500 | include: [ | 501 | include: [ |
501 | { | 502 | { |
@@ -519,7 +520,8 @@ export class ActorModel extends Model { | |||
519 | } | 520 | } |
520 | ] | 521 | ] |
521 | } | 522 | } |
522 | ] | 523 | ], |
524 | transaction | ||
523 | } | 525 | } |
524 | 526 | ||
525 | return ActorModel.unscoped().findOne(query) | 527 | return ActorModel.unscoped().findOne(query) |
@@ -566,7 +568,7 @@ export class ActorModel extends Model { | |||
566 | let image: ActivityIconObject | 568 | let image: ActivityIconObject |
567 | 569 | ||
568 | if (this.avatarId) { | 570 | if (this.avatarId) { |
569 | const extension = extname(this.Avatar.filename) | 571 | const extension = getLowercaseExtension(this.Avatar.filename) |
570 | 572 | ||
571 | icon = { | 573 | icon = { |
572 | type: 'Image', | 574 | type: 'Image', |
@@ -579,7 +581,7 @@ export class ActorModel extends Model { | |||
579 | 581 | ||
580 | if (this.bannerId) { | 582 | if (this.bannerId) { |
581 | const banner = (this as MActorAPChannel).Banner | 583 | const banner = (this as MActorAPChannel).Banner |
582 | const extension = extname(banner.filename) | 584 | const extension = getLowercaseExtension(banner.filename) |
583 | 585 | ||
584 | image = { | 586 | image = { |
585 | type: 'Image', | 587 | type: 'Image', |
diff --git a/server/models/application/application.ts b/server/models/application/application.ts index 21f8b1cbc..5531d134a 100644 --- a/server/models/application/application.ts +++ b/server/models/application/application.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import * as memoizee from 'memoizee' | ||
1 | import { AllowNull, Column, Default, DefaultScope, HasOne, IsInt, Model, Table } from 'sequelize-typescript' | 2 | import { AllowNull, Column, Default, DefaultScope, HasOne, IsInt, Model, Table } from 'sequelize-typescript' |
3 | import { AttributesOnly } from '@shared/core-utils' | ||
2 | import { AccountModel } from '../account/account' | 4 | import { AccountModel } from '../account/account' |
3 | import * as memoizee from 'memoizee' | ||
4 | 5 | ||
5 | export const getServerActor = memoizee(async function () { | 6 | export const getServerActor = memoizee(async function () { |
6 | const application = await ApplicationModel.load() | 7 | const application = await ApplicationModel.load() |
@@ -24,7 +25,7 @@ export const getServerActor = memoizee(async function () { | |||
24 | tableName: 'application', | 25 | tableName: 'application', |
25 | timestamps: false | 26 | timestamps: false |
26 | }) | 27 | }) |
27 | export class ApplicationModel extends Model { | 28 | export class ApplicationModel extends Model<Partial<AttributesOnly<ApplicationModel>>> { |
28 | 29 | ||
29 | @AllowNull(false) | 30 | @AllowNull(false) |
30 | @Default(0) | 31 | @Default(0) |
diff --git a/server/models/oauth/oauth-client.ts b/server/models/oauth/oauth-client.ts index 8dbc1c2f5..890954bdb 100644 --- a/server/models/oauth/oauth-client.ts +++ b/server/models/oauth/oauth-client.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { AllowNull, Column, CreatedAt, DataType, HasMany, Model, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { AllowNull, Column, CreatedAt, DataType, HasMany, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { AttributesOnly } from '@shared/core-utils' | ||
2 | import { OAuthTokenModel } from './oauth-token' | 3 | import { OAuthTokenModel } from './oauth-token' |
3 | 4 | ||
4 | @Table({ | 5 | @Table({ |
@@ -14,7 +15,7 @@ import { OAuthTokenModel } from './oauth-token' | |||
14 | } | 15 | } |
15 | ] | 16 | ] |
16 | }) | 17 | }) |
17 | export class OAuthClientModel extends Model { | 18 | export class OAuthClientModel extends Model<Partial<AttributesOnly<OAuthClientModel>>> { |
18 | 19 | ||
19 | @AllowNull(false) | 20 | @AllowNull(false) |
20 | @Column | 21 | @Column |
diff --git a/server/models/oauth/oauth-token.ts b/server/models/oauth/oauth-token.ts index 27e643aa7..af4b0ec42 100644 --- a/server/models/oauth/oauth-token.ts +++ b/server/models/oauth/oauth-token.ts | |||
@@ -15,10 +15,11 @@ import { | |||
15 | import { TokensCache } from '@server/lib/auth/tokens-cache' | 15 | import { TokensCache } from '@server/lib/auth/tokens-cache' |
16 | import { MUserAccountId } from '@server/types/models' | 16 | import { MUserAccountId } from '@server/types/models' |
17 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' | 17 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' |
18 | import { AttributesOnly } from '@shared/core-utils' | ||
18 | import { logger } from '../../helpers/logger' | 19 | import { logger } from '../../helpers/logger' |
19 | import { AccountModel } from '../account/account' | 20 | import { AccountModel } from '../account/account' |
20 | import { UserModel } from '../account/user' | 21 | import { ActorModel } from '../actor/actor' |
21 | import { ActorModel } from '../activitypub/actor' | 22 | import { UserModel } from '../user/user' |
22 | import { OAuthClientModel } from './oauth-client' | 23 | import { OAuthClientModel } from './oauth-client' |
23 | 24 | ||
24 | export type OAuthTokenInfo = { | 25 | export type OAuthTokenInfo = { |
@@ -78,7 +79,7 @@ enum ScopeNames { | |||
78 | } | 79 | } |
79 | ] | 80 | ] |
80 | }) | 81 | }) |
81 | export class OAuthTokenModel extends Model { | 82 | export class OAuthTokenModel extends Model<Partial<AttributesOnly<OAuthTokenModel>>> { |
82 | 83 | ||
83 | @AllowNull(false) | 84 | @AllowNull(false) |
84 | @Column | 85 | @Column |
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index 349dba513..ccda023e0 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts | |||
@@ -16,6 +16,7 @@ import { | |||
16 | } from 'sequelize-typescript' | 16 | } from 'sequelize-typescript' |
17 | import { getServerActor } from '@server/models/application/application' | 17 | import { getServerActor } from '@server/models/application/application' |
18 | import { MActor, MVideoForRedundancyAPI, MVideoRedundancy, MVideoRedundancyAP, MVideoRedundancyVideo } from '@server/types/models' | 18 | import { MActor, MVideoForRedundancyAPI, MVideoRedundancy, MVideoRedundancyAP, MVideoRedundancyVideo } from '@server/types/models' |
19 | import { AttributesOnly } from '@shared/core-utils' | ||
19 | import { VideoRedundanciesTarget } from '@shared/models/redundancy/video-redundancies-filters.model' | 20 | import { VideoRedundanciesTarget } from '@shared/models/redundancy/video-redundancies-filters.model' |
20 | import { | 21 | import { |
21 | FileRedundancyInformation, | 22 | FileRedundancyInformation, |
@@ -29,7 +30,7 @@ import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validato | |||
29 | import { logger } from '../../helpers/logger' | 30 | import { logger } from '../../helpers/logger' |
30 | import { CONFIG } from '../../initializers/config' | 31 | import { CONFIG } from '../../initializers/config' |
31 | import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants' | 32 | import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants' |
32 | import { ActorModel } from '../activitypub/actor' | 33 | import { ActorModel } from '../actor/actor' |
33 | import { ServerModel } from '../server/server' | 34 | import { ServerModel } from '../server/server' |
34 | import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils' | 35 | import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils' |
35 | import { ScheduleVideoUpdateModel } from '../video/schedule-video-update' | 36 | import { ScheduleVideoUpdateModel } from '../video/schedule-video-update' |
@@ -79,12 +80,15 @@ export enum ScopeNames { | |||
79 | fields: [ 'actorId' ] | 80 | fields: [ 'actorId' ] |
80 | }, | 81 | }, |
81 | { | 82 | { |
83 | fields: [ 'expiresOn' ] | ||
84 | }, | ||
85 | { | ||
82 | fields: [ 'url' ], | 86 | fields: [ 'url' ], |
83 | unique: true | 87 | unique: true |
84 | } | 88 | } |
85 | ] | 89 | ] |
86 | }) | 90 | }) |
87 | export class VideoRedundancyModel extends Model { | 91 | export class VideoRedundancyModel extends Model<Partial<AttributesOnly<VideoRedundancyModel>>> { |
88 | 92 | ||
89 | @CreatedAt | 93 | @CreatedAt |
90 | createdAt: Date | 94 | createdAt: Date |
diff --git a/server/models/server/plugin.ts b/server/models/server/plugin.ts index 80c8a6be5..a8de64dd4 100644 --- a/server/models/server/plugin.ts +++ b/server/models/server/plugin.ts | |||
@@ -1,9 +1,8 @@ | |||
1 | import { FindAndCountOptions, json, QueryTypes } from 'sequelize' | 1 | import { FindAndCountOptions, json, QueryTypes } from 'sequelize' |
2 | import { AllowNull, Column, CreatedAt, DataType, DefaultScope, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { AllowNull, Column, CreatedAt, DataType, DefaultScope, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { MPlugin, MPluginFormattable } from '@server/types/models' | 3 | import { MPlugin, MPluginFormattable } from '@server/types/models' |
4 | import { PeerTubePlugin } from '../../../shared/models/plugins/peertube-plugin.model' | 4 | import { AttributesOnly } from '@shared/core-utils' |
5 | import { PluginType } from '../../../shared/models/plugins/plugin.type' | 5 | import { PeerTubePlugin, PluginType, RegisterServerSettingOptions } from '../../../shared/models' |
6 | import { RegisterServerSettingOptions } from '../../../shared/models/plugins/register-server-setting.model' | ||
7 | import { | 6 | import { |
8 | isPluginDescriptionValid, | 7 | isPluginDescriptionValid, |
9 | isPluginHomepage, | 8 | isPluginHomepage, |
@@ -28,7 +27,7 @@ import { getSort, throwIfNotValid } from '../utils' | |||
28 | } | 27 | } |
29 | ] | 28 | ] |
30 | }) | 29 | }) |
31 | export class PluginModel extends Model { | 30 | export class PluginModel extends Model<Partial<AttributesOnly<PluginModel>>> { |
32 | 31 | ||
33 | @AllowNull(false) | 32 | @AllowNull(false) |
34 | @Is('PluginName', value => throwIfNotValid(value, isPluginNameValid, 'name')) | 33 | @Is('PluginName', value => throwIfNotValid(value, isPluginNameValid, 'name')) |
diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts index 4dc236537..b3579d589 100644 --- a/server/models/server/server-blocklist.ts +++ b/server/models/server/server-blocklist.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { Op } from 'sequelize' | 1 | import { Op } from 'sequelize' |
2 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models' | 3 | import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models' |
4 | import { AttributesOnly } from '@shared/core-utils' | ||
4 | import { ServerBlock } from '@shared/models' | 5 | import { ServerBlock } from '@shared/models' |
5 | import { AccountModel } from '../account/account' | 6 | import { AccountModel } from '../account/account' |
6 | import { getSort, searchAttribute } from '../utils' | 7 | import { getSort, searchAttribute } from '../utils' |
@@ -42,7 +43,7 @@ enum ScopeNames { | |||
42 | } | 43 | } |
43 | ] | 44 | ] |
44 | }) | 45 | }) |
45 | export class ServerBlocklistModel extends Model { | 46 | export class ServerBlocklistModel extends Model<Partial<AttributesOnly<ServerBlocklistModel>>> { |
46 | 47 | ||
47 | @CreatedAt | 48 | @CreatedAt |
48 | createdAt: Date | 49 | createdAt: Date |
diff --git a/server/models/server/server.ts b/server/models/server/server.ts index 0e58beeaf..0d3c092e0 100644 --- a/server/models/server/server.ts +++ b/server/models/server/server.ts | |||
@@ -1,7 +1,9 @@ | |||
1 | import { Transaction } from 'sequelize' | ||
1 | import { AllowNull, Column, CreatedAt, Default, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { AllowNull, Column, CreatedAt, Default, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { MServer, MServerFormattable } from '@server/types/models/server' | 3 | import { MServer, MServerFormattable } from '@server/types/models/server' |
4 | import { AttributesOnly } from '@shared/core-utils' | ||
3 | import { isHostValid } from '../../helpers/custom-validators/servers' | 5 | import { isHostValid } from '../../helpers/custom-validators/servers' |
4 | import { ActorModel } from '../activitypub/actor' | 6 | import { ActorModel } from '../actor/actor' |
5 | import { throwIfNotValid } from '../utils' | 7 | import { throwIfNotValid } from '../utils' |
6 | import { ServerBlocklistModel } from './server-blocklist' | 8 | import { ServerBlocklistModel } from './server-blocklist' |
7 | 9 | ||
@@ -14,7 +16,7 @@ import { ServerBlocklistModel } from './server-blocklist' | |||
14 | } | 16 | } |
15 | ] | 17 | ] |
16 | }) | 18 | }) |
17 | export class ServerModel extends Model { | 19 | export class ServerModel extends Model<Partial<AttributesOnly<ServerModel>>> { |
18 | 20 | ||
19 | @AllowNull(false) | 21 | @AllowNull(false) |
20 | @Is('Host', value => throwIfNotValid(value, isHostValid, 'valid host')) | 22 | @Is('Host', value => throwIfNotValid(value, isHostValid, 'valid host')) |
@@ -50,11 +52,12 @@ export class ServerModel extends Model { | |||
50 | }) | 52 | }) |
51 | BlockedByAccounts: ServerBlocklistModel[] | 53 | BlockedByAccounts: ServerBlocklistModel[] |
52 | 54 | ||
53 | static load (id: number): Promise<MServer> { | 55 | static load (id: number, transaction?: Transaction): Promise<MServer> { |
54 | const query = { | 56 | const query = { |
55 | where: { | 57 | where: { |
56 | id | 58 | id |
57 | } | 59 | }, |
60 | transaction | ||
58 | } | 61 | } |
59 | 62 | ||
60 | return ServerModel.findOne(query) | 63 | return ServerModel.findOne(query) |
diff --git a/server/models/server/tracker.ts b/server/models/server/tracker.ts index 97520f92d..c09fdd64b 100644 --- a/server/models/server/tracker.ts +++ b/server/models/server/tracker.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { AllowNull, BelongsToMany, Column, CreatedAt, Model, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { AllowNull, BelongsToMany, Column, CreatedAt, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { Transaction } from 'sequelize/types' | 2 | import { Transaction } from 'sequelize/types' |
3 | import { MTracker } from '@server/types/models/server/tracker' | 3 | import { MTracker } from '@server/types/models/server/tracker' |
4 | import { AttributesOnly } from '@shared/core-utils' | ||
4 | import { VideoModel } from '../video/video' | 5 | import { VideoModel } from '../video/video' |
5 | import { VideoTrackerModel } from './video-tracker' | 6 | import { VideoTrackerModel } from './video-tracker' |
6 | 7 | ||
@@ -13,7 +14,7 @@ import { VideoTrackerModel } from './video-tracker' | |||
13 | } | 14 | } |
14 | ] | 15 | ] |
15 | }) | 16 | }) |
16 | export class TrackerModel extends Model { | 17 | export class TrackerModel extends Model<Partial<AttributesOnly<TrackerModel>>> { |
17 | 18 | ||
18 | @AllowNull(false) | 19 | @AllowNull(false) |
19 | @Column | 20 | @Column |
diff --git a/server/models/server/video-tracker.ts b/server/models/server/video-tracker.ts index 367bf0117..c49fbd1c6 100644 --- a/server/models/server/video-tracker.ts +++ b/server/models/server/video-tracker.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { AttributesOnly } from '@shared/core-utils' | ||
2 | import { VideoModel } from '../video/video' | 3 | import { VideoModel } from '../video/video' |
3 | import { TrackerModel } from './tracker' | 4 | import { TrackerModel } from './tracker' |
4 | 5 | ||
@@ -13,7 +14,7 @@ import { TrackerModel } from './tracker' | |||
13 | } | 14 | } |
14 | ] | 15 | ] |
15 | }) | 16 | }) |
16 | export class VideoTrackerModel extends Model { | 17 | export class VideoTrackerModel extends Model<Partial<AttributesOnly<VideoTrackerModel>>> { |
17 | @CreatedAt | 18 | @CreatedAt |
18 | createdAt: Date | 19 | createdAt: Date |
19 | 20 | ||
diff --git a/server/models/account/user-notification-setting.ts b/server/models/user/user-notification-setting.ts index 138051528..bee7d7851 100644 --- a/server/models/account/user-notification-setting.ts +++ b/server/models/user/user-notification-setting.ts | |||
@@ -14,6 +14,7 @@ import { | |||
14 | } from 'sequelize-typescript' | 14 | } from 'sequelize-typescript' |
15 | import { TokensCache } from '@server/lib/auth/tokens-cache' | 15 | import { TokensCache } from '@server/lib/auth/tokens-cache' |
16 | import { MNotificationSettingFormattable } from '@server/types/models' | 16 | import { MNotificationSettingFormattable } from '@server/types/models' |
17 | import { AttributesOnly } from '@shared/core-utils' | ||
17 | import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' | 18 | import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' |
18 | import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' | 19 | import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' |
19 | import { throwIfNotValid } from '../utils' | 20 | import { throwIfNotValid } from '../utils' |
@@ -28,7 +29,7 @@ import { UserModel } from './user' | |||
28 | } | 29 | } |
29 | ] | 30 | ] |
30 | }) | 31 | }) |
31 | export class UserNotificationSettingModel extends Model { | 32 | export class UserNotificationSettingModel extends Model<Partial<AttributesOnly<UserNotificationSettingModel>>> { |
32 | 33 | ||
33 | @AllowNull(false) | 34 | @AllowNull(false) |
34 | @Default(null) | 35 | @Default(null) |
diff --git a/server/models/account/user-notification.ts b/server/models/user/user-notification.ts index 805095002..a7f84e9ca 100644 --- a/server/models/account/user-notification.ts +++ b/server/models/user/user-notification.ts | |||
@@ -1,14 +1,17 @@ | |||
1 | import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize' | 1 | import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize' |
2 | import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user' | 3 | import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user' |
4 | import { AttributesOnly } from '@shared/core-utils' | ||
4 | import { UserNotification, UserNotificationType } from '../../../shared' | 5 | import { UserNotification, UserNotificationType } from '../../../shared' |
5 | import { isBooleanValid } from '../../helpers/custom-validators/misc' | 6 | import { isBooleanValid } from '../../helpers/custom-validators/misc' |
6 | import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications' | 7 | import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications' |
7 | import { AbuseModel } from '../abuse/abuse' | 8 | import { AbuseModel } from '../abuse/abuse' |
8 | import { VideoAbuseModel } from '../abuse/video-abuse' | 9 | import { VideoAbuseModel } from '../abuse/video-abuse' |
9 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' | 10 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' |
10 | import { ActorModel } from '../activitypub/actor' | 11 | import { AccountModel } from '../account/account' |
11 | import { ActorFollowModel } from '../activitypub/actor-follow' | 12 | import { ActorModel } from '../actor/actor' |
13 | import { ActorFollowModel } from '../actor/actor-follow' | ||
14 | import { ActorImageModel } from '../actor/actor-image' | ||
12 | import { ApplicationModel } from '../application/application' | 15 | import { ApplicationModel } from '../application/application' |
13 | import { PluginModel } from '../server/plugin' | 16 | import { PluginModel } from '../server/plugin' |
14 | import { ServerModel } from '../server/server' | 17 | import { ServerModel } from '../server/server' |
@@ -18,8 +21,6 @@ import { VideoBlacklistModel } from '../video/video-blacklist' | |||
18 | import { VideoChannelModel } from '../video/video-channel' | 21 | import { VideoChannelModel } from '../video/video-channel' |
19 | import { VideoCommentModel } from '../video/video-comment' | 22 | import { VideoCommentModel } from '../video/video-comment' |
20 | import { VideoImportModel } from '../video/video-import' | 23 | import { VideoImportModel } from '../video/video-import' |
21 | import { AccountModel } from './account' | ||
22 | import { ActorImageModel } from './actor-image' | ||
23 | import { UserModel } from './user' | 24 | import { UserModel } from './user' |
24 | 25 | ||
25 | enum ScopeNames { | 26 | enum ScopeNames { |
@@ -286,7 +287,7 @@ function buildAccountInclude (required: boolean, withActor = false) { | |||
286 | } | 287 | } |
287 | ] as (ModelIndexesOptions & { where?: WhereOptions })[] | 288 | ] as (ModelIndexesOptions & { where?: WhereOptions })[] |
288 | }) | 289 | }) |
289 | export class UserNotificationModel extends Model { | 290 | export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNotificationModel>>> { |
290 | 291 | ||
291 | @AllowNull(false) | 292 | @AllowNull(false) |
292 | @Default(null) | 293 | @Default(null) |
diff --git a/server/models/account/user-video-history.ts b/server/models/user/user-video-history.ts index 6be1d65ea..e3dc4a062 100644 --- a/server/models/account/user-video-history.ts +++ b/server/models/user/user-video-history.ts | |||
@@ -1,8 +1,9 @@ | |||
1 | import { DestroyOptions, Op, Transaction } from 'sequelize' | ||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Model, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Model, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { MUserAccountId, MUserId } from '@server/types/models' | ||
4 | import { AttributesOnly } from '@shared/core-utils' | ||
2 | import { VideoModel } from '../video/video' | 5 | import { VideoModel } from '../video/video' |
3 | import { UserModel } from './user' | 6 | import { UserModel } from './user' |
4 | import { DestroyOptions, Op, Transaction } from 'sequelize' | ||
5 | import { MUserAccountId, MUserId } from '@server/types/models' | ||
6 | 7 | ||
7 | @Table({ | 8 | @Table({ |
8 | tableName: 'userVideoHistory', | 9 | tableName: 'userVideoHistory', |
@@ -19,7 +20,7 @@ import { MUserAccountId, MUserId } from '@server/types/models' | |||
19 | } | 20 | } |
20 | ] | 21 | ] |
21 | }) | 22 | }) |
22 | export class UserVideoHistoryModel extends Model { | 23 | export class UserVideoHistoryModel extends Model<Partial<AttributesOnly<UserVideoHistoryModel>>> { |
23 | @CreatedAt | 24 | @CreatedAt |
24 | createdAt: Date | 25 | createdAt: Date |
25 | 26 | ||
diff --git a/server/models/account/user.ts b/server/models/user/user.ts index 513455773..20696b1f4 100644 --- a/server/models/account/user.ts +++ b/server/models/user/user.ts | |||
@@ -31,6 +31,7 @@ import { | |||
31 | MUserWithNotificationSetting, | 31 | MUserWithNotificationSetting, |
32 | MVideoWithRights | 32 | MVideoWithRights |
33 | } from '@server/types/models' | 33 | } from '@server/types/models' |
34 | import { AttributesOnly } from '@shared/core-utils' | ||
34 | import { hasUserRight, USER_ROLE_LABELS } from '../../../shared/core-utils/users' | 35 | import { hasUserRight, USER_ROLE_LABELS } from '../../../shared/core-utils/users' |
35 | import { AbuseState, MyUser, UserRight, VideoPlaylistType, VideoPrivacy } from '../../../shared/models' | 36 | import { AbuseState, MyUser, UserRight, VideoPlaylistType, VideoPrivacy } from '../../../shared/models' |
36 | import { User, UserRole } from '../../../shared/models/users' | 37 | import { User, UserRole } from '../../../shared/models/users' |
@@ -60,8 +61,10 @@ import { | |||
60 | import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' | 61 | import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' |
61 | import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants' | 62 | import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants' |
62 | import { getThemeOrDefault } from '../../lib/plugins/theme-utils' | 63 | import { getThemeOrDefault } from '../../lib/plugins/theme-utils' |
63 | import { ActorModel } from '../activitypub/actor' | 64 | import { AccountModel } from '../account/account' |
64 | import { ActorFollowModel } from '../activitypub/actor-follow' | 65 | import { ActorModel } from '../actor/actor' |
66 | import { ActorFollowModel } from '../actor/actor-follow' | ||
67 | import { ActorImageModel } from '../actor/actor-image' | ||
65 | import { OAuthTokenModel } from '../oauth/oauth-token' | 68 | import { OAuthTokenModel } from '../oauth/oauth-token' |
66 | import { getSort, throwIfNotValid } from '../utils' | 69 | import { getSort, throwIfNotValid } from '../utils' |
67 | import { VideoModel } from '../video/video' | 70 | import { VideoModel } from '../video/video' |
@@ -69,9 +72,7 @@ import { VideoChannelModel } from '../video/video-channel' | |||
69 | import { VideoImportModel } from '../video/video-import' | 72 | import { VideoImportModel } from '../video/video-import' |
70 | import { VideoLiveModel } from '../video/video-live' | 73 | import { VideoLiveModel } from '../video/video-live' |
71 | import { VideoPlaylistModel } from '../video/video-playlist' | 74 | import { VideoPlaylistModel } from '../video/video-playlist' |
72 | import { AccountModel } from './account' | ||
73 | import { UserNotificationSettingModel } from './user-notification-setting' | 75 | import { UserNotificationSettingModel } from './user-notification-setting' |
74 | import { ActorImageModel } from './actor-image' | ||
75 | 76 | ||
76 | enum ScopeNames { | 77 | enum ScopeNames { |
77 | FOR_ME_API = 'FOR_ME_API', | 78 | FOR_ME_API = 'FOR_ME_API', |
@@ -233,7 +234,7 @@ enum ScopeNames { | |||
233 | } | 234 | } |
234 | ] | 235 | ] |
235 | }) | 236 | }) |
236 | export class UserModel extends Model { | 237 | export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> { |
237 | 238 | ||
238 | @AllowNull(true) | 239 | @AllowNull(true) |
239 | @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password', true)) | 240 | @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password', true)) |
diff --git a/server/models/utils.ts b/server/models/utils.ts index ec51c66bf..83b2b8f03 100644 --- a/server/models/utils.ts +++ b/server/models/utils.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import { literal, Op, OrderItem } from 'sequelize' | 1 | import { literal, Op, OrderItem, Sequelize } from 'sequelize' |
2 | import { Model, Sequelize } from 'sequelize-typescript' | ||
3 | import { Col } from 'sequelize/types/lib/utils' | 2 | import { Col } from 'sequelize/types/lib/utils' |
4 | import validator from 'validator' | 3 | import validator from 'validator' |
5 | 4 | ||
@@ -103,6 +102,10 @@ function getFollowsSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): | |||
103 | } | 102 | } |
104 | 103 | ||
105 | function isOutdated (model: { createdAt: Date, updatedAt: Date }, refreshInterval: number) { | 104 | function isOutdated (model: { createdAt: Date, updatedAt: Date }, refreshInterval: number) { |
105 | if (!model.createdAt || !model.updatedAt) { | ||
106 | throw new Error('Miss createdAt & updatedAt attribuets to model') | ||
107 | } | ||
108 | |||
106 | const now = Date.now() | 109 | const now = Date.now() |
107 | const createdAtTime = model.createdAt.getTime() | 110 | const createdAtTime = model.createdAt.getTime() |
108 | const updatedAtTime = model.updatedAt.getTime() | 111 | const updatedAtTime = model.updatedAt.getTime() |
@@ -195,11 +198,11 @@ function parseAggregateResult (result: any) { | |||
195 | return total | 198 | return total |
196 | } | 199 | } |
197 | 200 | ||
198 | const createSafeIn = (model: typeof Model, stringArr: (string | number)[]) => { | 201 | function createSafeIn (sequelize: Sequelize, stringArr: (string | number)[]) { |
199 | return stringArr.map(t => { | 202 | return stringArr.map(t => { |
200 | return t === null | 203 | return t === null |
201 | ? null | 204 | ? null |
202 | : model.sequelize.escape('' + t) | 205 | : sequelize.escape('' + t) |
203 | }).join(', ') | 206 | }).join(', ') |
204 | } | 207 | } |
205 | 208 | ||
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/formatter/video-format-utils.ts index 551cb2842..6b1e59063 100644 --- a/server/models/video/video-format-utils.ts +++ b/server/models/video/formatter/video-format-utils.ts | |||
@@ -1,17 +1,26 @@ | |||
1 | import { uuidToShort } from '@server/helpers/uuid' | ||
1 | import { generateMagnetUri } from '@server/helpers/webtorrent' | 2 | import { generateMagnetUri } from '@server/helpers/webtorrent' |
2 | import { getLocalVideoFileMetadataUrl } from '@server/lib/video-paths' | 3 | import { getLocalVideoFileMetadataUrl } from '@server/lib/video-paths' |
3 | import { VideoFile } from '@shared/models/videos/video-file.model' | 4 | import { VideoFile } from '@shared/models/videos/video-file.model' |
4 | import { ActivityTagObject, ActivityUrlObject, VideoObject } from '../../../shared/models/activitypub/objects' | 5 | import { ActivityTagObject, ActivityUrlObject, VideoObject } from '../../../../shared/models/activitypub/objects' |
5 | import { Video, VideoDetails } from '../../../shared/models/videos' | 6 | import { Video, VideoDetails } from '../../../../shared/models/videos' |
6 | import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model' | 7 | import { VideoStreamingPlaylist } from '../../../../shared/models/videos/video-streaming-playlist.model' |
7 | import { isArray } from '../../helpers/custom-validators/misc' | 8 | import { isArray } from '../../../helpers/custom-validators/misc' |
8 | import { MIMETYPES, WEBSERVER } from '../../initializers/constants' | 9 | import { |
10 | MIMETYPES, | ||
11 | VIDEO_CATEGORIES, | ||
12 | VIDEO_LANGUAGES, | ||
13 | VIDEO_LICENCES, | ||
14 | VIDEO_PRIVACIES, | ||
15 | VIDEO_STATES, | ||
16 | WEBSERVER | ||
17 | } from '../../../initializers/constants' | ||
9 | import { | 18 | import { |
10 | getLocalVideoCommentsActivityPubUrl, | 19 | getLocalVideoCommentsActivityPubUrl, |
11 | getLocalVideoDislikesActivityPubUrl, | 20 | getLocalVideoDislikesActivityPubUrl, |
12 | getLocalVideoLikesActivityPubUrl, | 21 | getLocalVideoLikesActivityPubUrl, |
13 | getLocalVideoSharesActivityPubUrl | 22 | getLocalVideoSharesActivityPubUrl |
14 | } from '../../lib/activitypub/url' | 23 | } from '../../../lib/activitypub/url' |
15 | import { | 24 | import { |
16 | MStreamingPlaylistRedundanciesOpt, | 25 | MStreamingPlaylistRedundanciesOpt, |
17 | MVideo, | 26 | MVideo, |
@@ -19,10 +28,9 @@ import { | |||
19 | MVideoFile, | 28 | MVideoFile, |
20 | MVideoFormattable, | 29 | MVideoFormattable, |
21 | MVideoFormattableDetails | 30 | MVideoFormattableDetails |
22 | } from '../../types/models' | 31 | } from '../../../types/models' |
23 | import { MVideoFileRedundanciesOpt } from '../../types/models/video/video-file' | 32 | import { MVideoFileRedundanciesOpt } from '../../../types/models/video/video-file' |
24 | import { VideoModel } from './video' | 33 | import { VideoCaptionModel } from '../video-caption' |
25 | import { VideoCaptionModel } from './video-caption' | ||
26 | 34 | ||
27 | export type VideoFormattingJSONOptions = { | 35 | export type VideoFormattingJSONOptions = { |
28 | completeDescription?: boolean | 36 | completeDescription?: boolean |
@@ -40,22 +48,24 @@ function videoModelToFormattedJSON (video: MVideoFormattable, options?: VideoFor | |||
40 | const videoObject: Video = { | 48 | const videoObject: Video = { |
41 | id: video.id, | 49 | id: video.id, |
42 | uuid: video.uuid, | 50 | uuid: video.uuid, |
51 | shortUUID: uuidToShort(video.uuid), | ||
52 | |||
43 | name: video.name, | 53 | name: video.name, |
44 | category: { | 54 | category: { |
45 | id: video.category, | 55 | id: video.category, |
46 | label: VideoModel.getCategoryLabel(video.category) | 56 | label: getCategoryLabel(video.category) |
47 | }, | 57 | }, |
48 | licence: { | 58 | licence: { |
49 | id: video.licence, | 59 | id: video.licence, |
50 | label: VideoModel.getLicenceLabel(video.licence) | 60 | label: getLicenceLabel(video.licence) |
51 | }, | 61 | }, |
52 | language: { | 62 | language: { |
53 | id: video.language, | 63 | id: video.language, |
54 | label: VideoModel.getLanguageLabel(video.language) | 64 | label: getLanguageLabel(video.language) |
55 | }, | 65 | }, |
56 | privacy: { | 66 | privacy: { |
57 | id: video.privacy, | 67 | id: video.privacy, |
58 | label: VideoModel.getPrivacyLabel(video.privacy) | 68 | label: getPrivacyLabel(video.privacy) |
59 | }, | 69 | }, |
60 | nsfw: video.nsfw, | 70 | nsfw: video.nsfw, |
61 | 71 | ||
@@ -93,7 +103,7 @@ function videoModelToFormattedJSON (video: MVideoFormattable, options?: VideoFor | |||
93 | if (options.additionalAttributes.state === true) { | 103 | if (options.additionalAttributes.state === true) { |
94 | videoObject.state = { | 104 | videoObject.state = { |
95 | id: video.state, | 105 | id: video.state, |
96 | label: VideoModel.getStateLabel(video.state) | 106 | label: getStateLabel(video.state) |
97 | } | 107 | } |
98 | } | 108 | } |
99 | 109 | ||
@@ -140,7 +150,7 @@ function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): Vid | |||
140 | waitTranscoding: video.waitTranscoding, | 150 | waitTranscoding: video.waitTranscoding, |
141 | state: { | 151 | state: { |
142 | id: video.state, | 152 | id: video.state, |
143 | label: VideoModel.getStateLabel(video.state) | 153 | label: getStateLabel(video.state) |
144 | }, | 154 | }, |
145 | 155 | ||
146 | trackerUrls: video.getTrackerUrls(), | 156 | trackerUrls: video.getTrackerUrls(), |
@@ -202,7 +212,7 @@ function videoFilesModelToFormattedJSON ( | |||
202 | return { | 212 | return { |
203 | resolution: { | 213 | resolution: { |
204 | id: videoFile.resolution, | 214 | id: videoFile.resolution, |
205 | label: videoFile.resolution + 'p' | 215 | label: videoFile.resolution === 0 ? 'Audio' : `${videoFile.resolution}p` |
206 | }, | 216 | }, |
207 | 217 | ||
208 | magnetUri: includeMagnet && videoFile.hasTorrent() | 218 | magnetUri: includeMagnet && videoFile.hasTorrent() |
@@ -283,7 +293,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject { | |||
283 | if (video.language) { | 293 | if (video.language) { |
284 | language = { | 294 | language = { |
285 | identifier: video.language, | 295 | identifier: video.language, |
286 | name: VideoModel.getLanguageLabel(video.language) | 296 | name: getLanguageLabel(video.language) |
287 | } | 297 | } |
288 | } | 298 | } |
289 | 299 | ||
@@ -291,7 +301,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject { | |||
291 | if (video.category) { | 301 | if (video.category) { |
292 | category = { | 302 | category = { |
293 | identifier: video.category + '', | 303 | identifier: video.category + '', |
294 | name: VideoModel.getCategoryLabel(video.category) | 304 | name: getCategoryLabel(video.category) |
295 | } | 305 | } |
296 | } | 306 | } |
297 | 307 | ||
@@ -299,7 +309,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject { | |||
299 | if (video.licence) { | 309 | if (video.licence) { |
300 | licence = { | 310 | licence = { |
301 | identifier: video.licence + '', | 311 | identifier: video.licence + '', |
302 | name: VideoModel.getLicenceLabel(video.licence) | 312 | name: getLicenceLabel(video.licence) |
303 | } | 313 | } |
304 | } | 314 | } |
305 | 315 | ||
@@ -425,10 +435,36 @@ function getActivityStreamDuration (duration: number) { | |||
425 | return 'PT' + duration + 'S' | 435 | return 'PT' + duration + 'S' |
426 | } | 436 | } |
427 | 437 | ||
438 | function getCategoryLabel (id: number) { | ||
439 | return VIDEO_CATEGORIES[id] || 'Misc' | ||
440 | } | ||
441 | |||
442 | function getLicenceLabel (id: number) { | ||
443 | return VIDEO_LICENCES[id] || 'Unknown' | ||
444 | } | ||
445 | |||
446 | function getLanguageLabel (id: string) { | ||
447 | return VIDEO_LANGUAGES[id] || 'Unknown' | ||
448 | } | ||
449 | |||
450 | function getPrivacyLabel (id: number) { | ||
451 | return VIDEO_PRIVACIES[id] || 'Unknown' | ||
452 | } | ||
453 | |||
454 | function getStateLabel (id: number) { | ||
455 | return VIDEO_STATES[id] || 'Unknown' | ||
456 | } | ||
457 | |||
428 | export { | 458 | export { |
429 | videoModelToFormattedJSON, | 459 | videoModelToFormattedJSON, |
430 | videoModelToFormattedDetailsJSON, | 460 | videoModelToFormattedDetailsJSON, |
431 | videoFilesModelToFormattedJSON, | 461 | videoFilesModelToFormattedJSON, |
432 | videoModelToActivityPubObject, | 462 | videoModelToActivityPubObject, |
433 | getActivityStreamDuration | 463 | getActivityStreamDuration, |
464 | |||
465 | getCategoryLabel, | ||
466 | getLicenceLabel, | ||
467 | getLanguageLabel, | ||
468 | getPrivacyLabel, | ||
469 | getStateLabel | ||
434 | } | 470 | } |
diff --git a/server/models/video/schedule-video-update.ts b/server/models/video/schedule-video-update.ts index 22b08e91a..d462c20c7 100644 --- a/server/models/video/schedule-video-update.ts +++ b/server/models/video/schedule-video-update.ts | |||
@@ -1,8 +1,9 @@ | |||
1 | import { Op, Transaction } from 'sequelize' | ||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { ScopeNames as VideoScopeNames, VideoModel } from './video' | 3 | import { MScheduleVideoUpdateFormattable, MScheduleVideoUpdate } from '@server/types/models' |
4 | import { AttributesOnly } from '@shared/core-utils' | ||
3 | import { VideoPrivacy } from '../../../shared/models/videos' | 5 | import { VideoPrivacy } from '../../../shared/models/videos' |
4 | import { Op, Transaction } from 'sequelize' | 6 | import { VideoModel } from './video' |
5 | import { MScheduleVideoUpdateFormattable, MScheduleVideoUpdateVideoAll } from '@server/types/models' | ||
6 | 7 | ||
7 | @Table({ | 8 | @Table({ |
8 | tableName: 'scheduleVideoUpdate', | 9 | tableName: 'scheduleVideoUpdate', |
@@ -16,7 +17,7 @@ import { MScheduleVideoUpdateFormattable, MScheduleVideoUpdateVideoAll } from '@ | |||
16 | } | 17 | } |
17 | ] | 18 | ] |
18 | }) | 19 | }) |
19 | export class ScheduleVideoUpdateModel extends Model { | 20 | export class ScheduleVideoUpdateModel extends Model<Partial<AttributesOnly<ScheduleVideoUpdateModel>>> { |
20 | 21 | ||
21 | @AllowNull(false) | 22 | @AllowNull(false) |
22 | @Default(null) | 23 | @Default(null) |
@@ -61,31 +62,17 @@ export class ScheduleVideoUpdateModel extends Model { | |||
61 | .then(res => !!res) | 62 | .then(res => !!res) |
62 | } | 63 | } |
63 | 64 | ||
64 | static listVideosToUpdate (t: Transaction) { | 65 | static listVideosToUpdate (transaction?: Transaction) { |
65 | const query = { | 66 | const query = { |
66 | where: { | 67 | where: { |
67 | updateAt: { | 68 | updateAt: { |
68 | [Op.lte]: new Date() | 69 | [Op.lte]: new Date() |
69 | } | 70 | } |
70 | }, | 71 | }, |
71 | include: [ | 72 | transaction |
72 | { | ||
73 | model: VideoModel.scope( | ||
74 | [ | ||
75 | VideoScopeNames.WITH_WEBTORRENT_FILES, | ||
76 | VideoScopeNames.WITH_STREAMING_PLAYLISTS, | ||
77 | VideoScopeNames.WITH_ACCOUNT_DETAILS, | ||
78 | VideoScopeNames.WITH_BLACKLISTED, | ||
79 | VideoScopeNames.WITH_THUMBNAILS, | ||
80 | VideoScopeNames.WITH_TAGS | ||
81 | ] | ||
82 | ) | ||
83 | } | ||
84 | ], | ||
85 | transaction: t | ||
86 | } | 73 | } |
87 | 74 | ||
88 | return ScheduleVideoUpdateModel.findAll<MScheduleVideoUpdateVideoAll>(query) | 75 | return ScheduleVideoUpdateModel.findAll<MScheduleVideoUpdate>(query) |
89 | } | 76 | } |
90 | 77 | ||
91 | static deleteByVideoId (videoId: number, t: Transaction) { | 78 | static deleteByVideoId (videoId: number, t: Transaction) { |
diff --git a/server/models/video/sql/shared/abstract-videos-model-query-builder.ts b/server/models/video/sql/shared/abstract-videos-model-query-builder.ts new file mode 100644 index 000000000..0d7e64574 --- /dev/null +++ b/server/models/video/sql/shared/abstract-videos-model-query-builder.ts | |||
@@ -0,0 +1,300 @@ | |||
1 | import validator from 'validator' | ||
2 | import { AbstractVideosQueryBuilder } from './abstract-videos-query-builder' | ||
3 | import { VideoTables } from './video-tables' | ||
4 | |||
5 | /** | ||
6 | * | ||
7 | * Abstract builder to create SQL query and fetch video models | ||
8 | * | ||
9 | */ | ||
10 | |||
11 | export class AbstractVideosModelQueryBuilder extends AbstractVideosQueryBuilder { | ||
12 | protected attributes: { [key: string]: string } = {} | ||
13 | |||
14 | protected joins = '' | ||
15 | protected where: string | ||
16 | |||
17 | protected tables: VideoTables | ||
18 | |||
19 | constructor (protected readonly mode: 'list' | 'get') { | ||
20 | super() | ||
21 | |||
22 | this.tables = new VideoTables(this.mode) | ||
23 | } | ||
24 | |||
25 | protected buildSelect () { | ||
26 | return 'SELECT ' + Object.keys(this.attributes).map(key => { | ||
27 | const value = this.attributes[key] | ||
28 | if (value) return `${key} AS ${value}` | ||
29 | |||
30 | return key | ||
31 | }).join(', ') | ||
32 | } | ||
33 | |||
34 | protected includeChannels () { | ||
35 | this.addJoin('INNER JOIN "videoChannel" AS "VideoChannel" ON "video"."channelId" = "VideoChannel"."id"') | ||
36 | this.addJoin('INNER JOIN "actor" AS "VideoChannel->Actor" ON "VideoChannel"."actorId" = "VideoChannel->Actor"."id"') | ||
37 | |||
38 | this.addJoin( | ||
39 | 'LEFT OUTER JOIN "server" AS "VideoChannel->Actor->Server" ON "VideoChannel->Actor"."serverId" = "VideoChannel->Actor->Server"."id"' | ||
40 | ) | ||
41 | |||
42 | this.addJoin( | ||
43 | 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Actor->Avatar" ' + | ||
44 | 'ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"' | ||
45 | ) | ||
46 | |||
47 | this.attributes = { | ||
48 | ...this.attributes, | ||
49 | |||
50 | ...this.buildAttributesObject('VideoChannel', this.tables.getChannelAttributes()), | ||
51 | ...this.buildActorInclude('VideoChannel->Actor'), | ||
52 | ...this.buildAvatarInclude('VideoChannel->Actor->Avatar'), | ||
53 | ...this.buildServerInclude('VideoChannel->Actor->Server') | ||
54 | } | ||
55 | } | ||
56 | |||
57 | protected includeAccounts () { | ||
58 | this.addJoin('INNER JOIN "account" AS "VideoChannel->Account" ON "VideoChannel"."accountId" = "VideoChannel->Account"."id"') | ||
59 | this.addJoin( | ||
60 | 'INNER JOIN "actor" AS "VideoChannel->Account->Actor" ON "VideoChannel->Account"."actorId" = "VideoChannel->Account->Actor"."id"' | ||
61 | ) | ||
62 | |||
63 | this.addJoin( | ||
64 | 'LEFT OUTER JOIN "server" AS "VideoChannel->Account->Actor->Server" ' + | ||
65 | 'ON "VideoChannel->Account->Actor"."serverId" = "VideoChannel->Account->Actor->Server"."id"' | ||
66 | ) | ||
67 | |||
68 | this.addJoin( | ||
69 | 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Account->Actor->Avatar" ' + | ||
70 | 'ON "VideoChannel->Account->Actor"."avatarId" = "VideoChannel->Account->Actor->Avatar"."id"' | ||
71 | ) | ||
72 | |||
73 | this.attributes = { | ||
74 | ...this.attributes, | ||
75 | |||
76 | ...this.buildAttributesObject('VideoChannel->Account', this.tables.getAccountAttributes()), | ||
77 | ...this.buildActorInclude('VideoChannel->Account->Actor'), | ||
78 | ...this.buildAvatarInclude('VideoChannel->Account->Actor->Avatar'), | ||
79 | ...this.buildServerInclude('VideoChannel->Account->Actor->Server') | ||
80 | } | ||
81 | } | ||
82 | |||
83 | protected includeOwnerUser () { | ||
84 | this.addJoin('INNER JOIN "videoChannel" AS "VideoChannel" ON "video"."channelId" = "VideoChannel"."id"') | ||
85 | this.addJoin('INNER JOIN "account" AS "VideoChannel->Account" ON "VideoChannel"."accountId" = "VideoChannel->Account"."id"') | ||
86 | |||
87 | this.attributes = { | ||
88 | ...this.attributes, | ||
89 | |||
90 | ...this.buildAttributesObject('VideoChannel', this.tables.getChannelAttributes()), | ||
91 | ...this.buildAttributesObject('VideoChannel->Account', this.tables.getUserAccountAttributes()) | ||
92 | } | ||
93 | } | ||
94 | |||
95 | protected includeThumbnails () { | ||
96 | this.addJoin('LEFT OUTER JOIN "thumbnail" AS "Thumbnails" ON "video"."id" = "Thumbnails"."videoId"') | ||
97 | |||
98 | this.attributes = { | ||
99 | ...this.attributes, | ||
100 | |||
101 | ...this.buildAttributesObject('Thumbnails', this.tables.getThumbnailAttributes()) | ||
102 | } | ||
103 | } | ||
104 | |||
105 | protected includeWebtorrentFiles () { | ||
106 | this.addJoin('LEFT JOIN "videoFile" AS "VideoFiles" ON "VideoFiles"."videoId" = "video"."id"') | ||
107 | |||
108 | this.attributes = { | ||
109 | ...this.attributes, | ||
110 | |||
111 | ...this.buildAttributesObject('VideoFiles', this.tables.getFileAttributes()) | ||
112 | } | ||
113 | } | ||
114 | |||
115 | protected includeStreamingPlaylistFiles () { | ||
116 | this.addJoin( | ||
117 | 'LEFT JOIN "videoStreamingPlaylist" AS "VideoStreamingPlaylists" ON "VideoStreamingPlaylists"."videoId" = "video"."id"' | ||
118 | ) | ||
119 | |||
120 | this.addJoin( | ||
121 | 'LEFT JOIN "videoFile" AS "VideoStreamingPlaylists->VideoFiles" ' + | ||
122 | 'ON "VideoStreamingPlaylists->VideoFiles"."videoStreamingPlaylistId" = "VideoStreamingPlaylists"."id"' | ||
123 | ) | ||
124 | |||
125 | this.attributes = { | ||
126 | ...this.attributes, | ||
127 | |||
128 | ...this.buildAttributesObject('VideoStreamingPlaylists', this.tables.getStreamingPlaylistAttributes()), | ||
129 | ...this.buildAttributesObject('VideoStreamingPlaylists->VideoFiles', this.tables.getFileAttributes()) | ||
130 | } | ||
131 | } | ||
132 | |||
133 | protected includeUserHistory (userId: number) { | ||
134 | this.addJoin( | ||
135 | 'LEFT OUTER JOIN "userVideoHistory" ' + | ||
136 | 'ON "video"."id" = "userVideoHistory"."videoId" AND "userVideoHistory"."userId" = :userVideoHistoryId' | ||
137 | ) | ||
138 | |||
139 | this.replacements.userVideoHistoryId = userId | ||
140 | |||
141 | this.attributes = { | ||
142 | ...this.attributes, | ||
143 | |||
144 | ...this.buildAttributesObject('userVideoHistory', this.tables.getUserHistoryAttributes()) | ||
145 | } | ||
146 | } | ||
147 | |||
148 | protected includePlaylist (playlistId: number) { | ||
149 | this.addJoin( | ||
150 | 'INNER JOIN "videoPlaylistElement" as "VideoPlaylistElement" ON "videoPlaylistElement"."videoId" = "video"."id" ' + | ||
151 | 'AND "VideoPlaylistElement"."videoPlaylistId" = :videoPlaylistId' | ||
152 | ) | ||
153 | |||
154 | this.replacements.videoPlaylistId = playlistId | ||
155 | |||
156 | this.attributes = { | ||
157 | ...this.attributes, | ||
158 | |||
159 | ...this.buildAttributesObject('VideoPlaylistElement', this.tables.getPlaylistAttributes()) | ||
160 | } | ||
161 | } | ||
162 | |||
163 | protected includeTags () { | ||
164 | this.addJoin( | ||
165 | 'LEFT OUTER JOIN (' + | ||
166 | '"videoTag" AS "Tags->VideoTagModel" INNER JOIN "tag" AS "Tags" ON "Tags"."id" = "Tags->VideoTagModel"."tagId"' + | ||
167 | ') ' + | ||
168 | 'ON "video"."id" = "Tags->VideoTagModel"."videoId"' | ||
169 | ) | ||
170 | |||
171 | this.attributes = { | ||
172 | ...this.attributes, | ||
173 | |||
174 | ...this.buildAttributesObject('Tags', this.tables.getTagAttributes()), | ||
175 | ...this.buildAttributesObject('Tags->VideoTagModel', this.tables.getVideoTagAttributes()) | ||
176 | } | ||
177 | } | ||
178 | |||
179 | protected includeBlacklisted () { | ||
180 | this.addJoin( | ||
181 | 'LEFT OUTER JOIN "videoBlacklist" AS "VideoBlacklist" ON "video"."id" = "VideoBlacklist"."videoId"' | ||
182 | ) | ||
183 | |||
184 | this.attributes = { | ||
185 | ...this.attributes, | ||
186 | |||
187 | ...this.buildAttributesObject('VideoBlacklist', this.tables.getBlacklistedAttributes()) | ||
188 | } | ||
189 | } | ||
190 | |||
191 | protected includeScheduleUpdate () { | ||
192 | this.addJoin( | ||
193 | 'LEFT OUTER JOIN "scheduleVideoUpdate" AS "ScheduleVideoUpdate" ON "video"."id" = "ScheduleVideoUpdate"."videoId"' | ||
194 | ) | ||
195 | |||
196 | this.attributes = { | ||
197 | ...this.attributes, | ||
198 | |||
199 | ...this.buildAttributesObject('ScheduleVideoUpdate', this.tables.getScheduleUpdateAttributes()) | ||
200 | } | ||
201 | } | ||
202 | |||
203 | protected includeLive () { | ||
204 | this.addJoin( | ||
205 | 'LEFT OUTER JOIN "videoLive" AS "VideoLive" ON "video"."id" = "VideoLive"."videoId"' | ||
206 | ) | ||
207 | |||
208 | this.attributes = { | ||
209 | ...this.attributes, | ||
210 | |||
211 | ...this.buildAttributesObject('VideoLive', this.tables.getLiveAttributes()) | ||
212 | } | ||
213 | } | ||
214 | |||
215 | protected includeTrackers () { | ||
216 | this.addJoin( | ||
217 | 'LEFT OUTER JOIN (' + | ||
218 | '"videoTracker" AS "Trackers->VideoTrackerModel" ' + | ||
219 | 'INNER JOIN "tracker" AS "Trackers" ON "Trackers"."id" = "Trackers->VideoTrackerModel"."trackerId"' + | ||
220 | ') ON "video"."id" = "Trackers->VideoTrackerModel"."videoId"' | ||
221 | ) | ||
222 | |||
223 | this.attributes = { | ||
224 | ...this.attributes, | ||
225 | |||
226 | ...this.buildAttributesObject('Trackers', this.tables.getTrackerAttributes()), | ||
227 | ...this.buildAttributesObject('Trackers->VideoTrackerModel', this.tables.getVideoTrackerAttributes()) | ||
228 | } | ||
229 | } | ||
230 | |||
231 | protected includeWebTorrentRedundancies () { | ||
232 | this.addJoin( | ||
233 | 'LEFT OUTER JOIN "videoRedundancy" AS "VideoFiles->RedundancyVideos" ON ' + | ||
234 | '"VideoFiles"."id" = "VideoFiles->RedundancyVideos"."videoFileId"' | ||
235 | ) | ||
236 | |||
237 | this.attributes = { | ||
238 | ...this.attributes, | ||
239 | |||
240 | ...this.buildAttributesObject('VideoFiles->RedundancyVideos', this.tables.getRedundancyAttributes()) | ||
241 | } | ||
242 | } | ||
243 | |||
244 | protected includeStreamingPlaylistRedundancies () { | ||
245 | this.addJoin( | ||
246 | 'LEFT OUTER JOIN "videoRedundancy" AS "VideoStreamingPlaylists->RedundancyVideos" ' + | ||
247 | 'ON "VideoStreamingPlaylists"."id" = "VideoStreamingPlaylists->RedundancyVideos"."videoStreamingPlaylistId"' | ||
248 | ) | ||
249 | |||
250 | this.attributes = { | ||
251 | ...this.attributes, | ||
252 | |||
253 | ...this.buildAttributesObject('VideoStreamingPlaylists->RedundancyVideos', this.tables.getRedundancyAttributes()) | ||
254 | } | ||
255 | } | ||
256 | |||
257 | protected buildActorInclude (prefixKey: string) { | ||
258 | return this.buildAttributesObject(prefixKey, this.tables.getActorAttributes()) | ||
259 | } | ||
260 | |||
261 | protected buildAvatarInclude (prefixKey: string) { | ||
262 | return this.buildAttributesObject(prefixKey, this.tables.getAvatarAttributes()) | ||
263 | } | ||
264 | |||
265 | protected buildServerInclude (prefixKey: string) { | ||
266 | return this.buildAttributesObject(prefixKey, this.tables.getServerAttributes()) | ||
267 | } | ||
268 | |||
269 | protected buildAttributesObject (prefixKey: string, attributeKeys: string[]) { | ||
270 | const result: { [id: string]: string} = {} | ||
271 | |||
272 | const prefixValue = prefixKey.replace(/->/g, '.') | ||
273 | |||
274 | for (const attribute of attributeKeys) { | ||
275 | result[`"${prefixKey}"."${attribute}"`] = `"${prefixValue}.${attribute}"` | ||
276 | } | ||
277 | |||
278 | return result | ||
279 | } | ||
280 | |||
281 | protected whereId (options: { id?: string | number, url?: string }) { | ||
282 | if (options.url) { | ||
283 | this.where = 'WHERE "video"."url" = :videoUrl' | ||
284 | this.replacements.videoUrl = options.url | ||
285 | return | ||
286 | } | ||
287 | |||
288 | if (validator.isInt('' + options.id)) { | ||
289 | this.where = 'WHERE "video".id = :videoId' | ||
290 | } else { | ||
291 | this.where = 'WHERE uuid = :videoId' | ||
292 | } | ||
293 | |||
294 | this.replacements.videoId = options.id | ||
295 | } | ||
296 | |||
297 | protected addJoin (join: string) { | ||
298 | this.joins += join + ' ' | ||
299 | } | ||
300 | } | ||
diff --git a/server/models/video/sql/shared/abstract-videos-query-builder.ts b/server/models/video/sql/shared/abstract-videos-query-builder.ts new file mode 100644 index 000000000..09776bcb0 --- /dev/null +++ b/server/models/video/sql/shared/abstract-videos-query-builder.ts | |||
@@ -0,0 +1,26 @@ | |||
1 | import { QueryTypes, Sequelize, Transaction } from 'sequelize' | ||
2 | |||
3 | /** | ||
4 | * | ||
5 | * Abstact builder to run video SQL queries | ||
6 | * | ||
7 | */ | ||
8 | |||
9 | export class AbstractVideosQueryBuilder { | ||
10 | protected sequelize: Sequelize | ||
11 | |||
12 | protected query: string | ||
13 | protected replacements: any = {} | ||
14 | |||
15 | protected runQuery (options: { transaction?: Transaction, logging?: boolean } = {}) { | ||
16 | const queryOptions = { | ||
17 | transaction: options.transaction, | ||
18 | logging: options.logging, | ||
19 | replacements: this.replacements, | ||
20 | type: QueryTypes.SELECT as QueryTypes.SELECT, | ||
21 | nest: false | ||
22 | } | ||
23 | |||
24 | return this.sequelize.query<any>(this.query, queryOptions) | ||
25 | } | ||
26 | } | ||
diff --git a/server/models/video/sql/shared/video-file-query-builder.ts b/server/models/video/sql/shared/video-file-query-builder.ts new file mode 100644 index 000000000..6b15c3b69 --- /dev/null +++ b/server/models/video/sql/shared/video-file-query-builder.ts | |||
@@ -0,0 +1,69 @@ | |||
1 | import { Sequelize } from 'sequelize' | ||
2 | import { BuildVideoGetQueryOptions } from '../video-model-get-query-builder' | ||
3 | import { AbstractVideosModelQueryBuilder } from './abstract-videos-model-query-builder' | ||
4 | |||
5 | /** | ||
6 | * | ||
7 | * Fetch files (webtorrent and streaming playlist) according to a video | ||
8 | * | ||
9 | */ | ||
10 | |||
11 | export class VideoFileQueryBuilder extends AbstractVideosModelQueryBuilder { | ||
12 | protected attributes: { [key: string]: string } | ||
13 | |||
14 | constructor (protected readonly sequelize: Sequelize) { | ||
15 | super('get') | ||
16 | } | ||
17 | |||
18 | queryWebTorrentVideos (options: BuildVideoGetQueryOptions) { | ||
19 | this.buildWebtorrentFilesQuery(options) | ||
20 | |||
21 | return this.runQuery(options) | ||
22 | } | ||
23 | |||
24 | queryStreamingPlaylistVideos (options: BuildVideoGetQueryOptions) { | ||
25 | this.buildVideoStreamingPlaylistFilesQuery(options) | ||
26 | |||
27 | return this.runQuery(options) | ||
28 | } | ||
29 | |||
30 | private buildWebtorrentFilesQuery (options: BuildVideoGetQueryOptions) { | ||
31 | this.attributes = { | ||
32 | '"video"."id"': '' | ||
33 | } | ||
34 | |||
35 | this.includeWebtorrentFiles() | ||
36 | |||
37 | if (this.shouldIncludeRedundancies(options)) { | ||
38 | this.includeWebTorrentRedundancies() | ||
39 | } | ||
40 | |||
41 | this.whereId(options) | ||
42 | |||
43 | this.query = this.buildQuery() | ||
44 | } | ||
45 | |||
46 | private buildVideoStreamingPlaylistFilesQuery (options: BuildVideoGetQueryOptions) { | ||
47 | this.attributes = { | ||
48 | '"video"."id"': '' | ||
49 | } | ||
50 | |||
51 | this.includeStreamingPlaylistFiles() | ||
52 | |||
53 | if (this.shouldIncludeRedundancies(options)) { | ||
54 | this.includeStreamingPlaylistRedundancies() | ||
55 | } | ||
56 | |||
57 | this.whereId(options) | ||
58 | |||
59 | this.query = this.buildQuery() | ||
60 | } | ||
61 | |||
62 | private buildQuery () { | ||
63 | return `${this.buildSelect()} FROM "video" ${this.joins} ${this.where}` | ||
64 | } | ||
65 | |||
66 | private shouldIncludeRedundancies (options: BuildVideoGetQueryOptions) { | ||
67 | return options.type === 'api' | ||
68 | } | ||
69 | } | ||
diff --git a/server/models/video/sql/shared/video-model-builder.ts b/server/models/video/sql/shared/video-model-builder.ts new file mode 100644 index 000000000..e7e2aa1ca --- /dev/null +++ b/server/models/video/sql/shared/video-model-builder.ts | |||
@@ -0,0 +1,333 @@ | |||
1 | |||
2 | import { AccountModel } from '@server/models/account/account' | ||
3 | import { ActorModel } from '@server/models/actor/actor' | ||
4 | import { ActorImageModel } from '@server/models/actor/actor-image' | ||
5 | import { VideoRedundancyModel } from '@server/models/redundancy/video-redundancy' | ||
6 | import { ServerModel } from '@server/models/server/server' | ||
7 | import { TrackerModel } from '@server/models/server/tracker' | ||
8 | import { UserVideoHistoryModel } from '@server/models/user/user-video-history' | ||
9 | import { ScheduleVideoUpdateModel } from '../../schedule-video-update' | ||
10 | import { TagModel } from '../../tag' | ||
11 | import { ThumbnailModel } from '../../thumbnail' | ||
12 | import { VideoModel } from '../../video' | ||
13 | import { VideoBlacklistModel } from '../../video-blacklist' | ||
14 | import { VideoChannelModel } from '../../video-channel' | ||
15 | import { VideoFileModel } from '../../video-file' | ||
16 | import { VideoLiveModel } from '../../video-live' | ||
17 | import { VideoStreamingPlaylistModel } from '../../video-streaming-playlist' | ||
18 | import { VideoTables } from './video-tables' | ||
19 | |||
20 | type SQLRow = { [id: string]: string | number } | ||
21 | |||
22 | /** | ||
23 | * | ||
24 | * Build video models from SQL rows | ||
25 | * | ||
26 | */ | ||
27 | |||
28 | export class VideoModelBuilder { | ||
29 | private videosMemo: { [ id: number ]: VideoModel } | ||
30 | private videoStreamingPlaylistMemo: { [ id: number ]: VideoStreamingPlaylistModel } | ||
31 | private videoFileMemo: { [ id: number ]: VideoFileModel } | ||
32 | |||
33 | private thumbnailsDone: Set<any> | ||
34 | private historyDone: Set<any> | ||
35 | private blacklistDone: Set<any> | ||
36 | private liveDone: Set<any> | ||
37 | private redundancyDone: Set<any> | ||
38 | private scheduleVideoUpdateDone: Set<any> | ||
39 | |||
40 | private trackersDone: Set<string> | ||
41 | private tagsDone: Set<string> | ||
42 | |||
43 | private videos: VideoModel[] | ||
44 | |||
45 | private readonly buildOpts = { raw: true, isNewRecord: false } | ||
46 | |||
47 | constructor ( | ||
48 | readonly mode: 'get' | 'list', | ||
49 | readonly tables: VideoTables | ||
50 | ) { | ||
51 | |||
52 | } | ||
53 | |||
54 | buildVideosFromRows (rows: SQLRow[], rowsWebTorrentFiles?: SQLRow[], rowsStreamingPlaylist?: SQLRow[]) { | ||
55 | this.reinit() | ||
56 | |||
57 | for (const row of rows) { | ||
58 | this.buildVideoAndAccount(row) | ||
59 | |||
60 | const videoModel = this.videosMemo[row.id] | ||
61 | |||
62 | this.setUserHistory(row, videoModel) | ||
63 | this.addThumbnail(row, videoModel) | ||
64 | |||
65 | if (!rowsWebTorrentFiles) { | ||
66 | this.addWebTorrentFile(row, videoModel) | ||
67 | } | ||
68 | |||
69 | if (!rowsStreamingPlaylist) { | ||
70 | this.addStreamingPlaylist(row, videoModel) | ||
71 | this.addStreamingPlaylistFile(row) | ||
72 | } | ||
73 | |||
74 | if (this.mode === 'get') { | ||
75 | this.addTag(row, videoModel) | ||
76 | this.addTracker(row, videoModel) | ||
77 | this.setBlacklisted(row, videoModel) | ||
78 | this.setScheduleVideoUpdate(row, videoModel) | ||
79 | this.setLive(row, videoModel) | ||
80 | } | ||
81 | } | ||
82 | |||
83 | this.grabSeparateWebTorrentFiles(rowsWebTorrentFiles) | ||
84 | this.grabSeparateStreamingPlaylistFiles(rowsStreamingPlaylist) | ||
85 | |||
86 | return this.videos | ||
87 | } | ||
88 | |||
89 | private reinit () { | ||
90 | this.videosMemo = {} | ||
91 | this.videoStreamingPlaylistMemo = {} | ||
92 | this.videoFileMemo = {} | ||
93 | |||
94 | this.thumbnailsDone = new Set<number>() | ||
95 | this.historyDone = new Set<number>() | ||
96 | this.blacklistDone = new Set<number>() | ||
97 | this.liveDone = new Set<number>() | ||
98 | this.redundancyDone = new Set<number>() | ||
99 | this.scheduleVideoUpdateDone = new Set<number>() | ||
100 | |||
101 | this.trackersDone = new Set<string>() | ||
102 | this.tagsDone = new Set<string>() | ||
103 | |||
104 | this.videos = [] | ||
105 | } | ||
106 | |||
107 | private grabSeparateWebTorrentFiles (rowsWebTorrentFiles?: SQLRow[]) { | ||
108 | if (!rowsWebTorrentFiles) return | ||
109 | |||
110 | for (const row of rowsWebTorrentFiles) { | ||
111 | const id = row['VideoFiles.id'] | ||
112 | if (!id) continue | ||
113 | |||
114 | const videoModel = this.videosMemo[row.id] | ||
115 | this.addWebTorrentFile(row, videoModel) | ||
116 | this.addRedundancy(row, 'VideoFiles', this.videoFileMemo[id]) | ||
117 | } | ||
118 | } | ||
119 | |||
120 | private grabSeparateStreamingPlaylistFiles (rowsStreamingPlaylist?: SQLRow[]) { | ||
121 | if (!rowsStreamingPlaylist) return | ||
122 | |||
123 | for (const row of rowsStreamingPlaylist || []) { | ||
124 | const id = row['VideoStreamingPlaylists.id'] | ||
125 | if (!id) continue | ||
126 | |||
127 | const videoModel = this.videosMemo[row.id] | ||
128 | |||
129 | this.addStreamingPlaylist(row, videoModel) | ||
130 | this.addStreamingPlaylistFile(row) | ||
131 | this.addRedundancy(row, 'VideoStreamingPlaylists', this.videoStreamingPlaylistMemo[id]) | ||
132 | } | ||
133 | } | ||
134 | |||
135 | private buildVideoAndAccount (row: SQLRow) { | ||
136 | if (this.videosMemo[row.id]) return | ||
137 | |||
138 | const videoModel = new VideoModel(this.grab(row, this.tables.getVideoAttributes(), ''), this.buildOpts) | ||
139 | |||
140 | videoModel.UserVideoHistories = [] | ||
141 | videoModel.Thumbnails = [] | ||
142 | videoModel.VideoFiles = [] | ||
143 | videoModel.VideoStreamingPlaylists = [] | ||
144 | videoModel.Tags = [] | ||
145 | videoModel.Trackers = [] | ||
146 | |||
147 | this.buildAccount(row, videoModel) | ||
148 | |||
149 | this.videosMemo[row.id] = videoModel | ||
150 | |||
151 | // Keep rows order | ||
152 | this.videos.push(videoModel) | ||
153 | } | ||
154 | |||
155 | private buildAccount (row: SQLRow, videoModel: VideoModel) { | ||
156 | const id = row['VideoChannel.Account.id'] | ||
157 | if (!id) return | ||
158 | |||
159 | const channelModel = new VideoChannelModel(this.grab(row, this.tables.getChannelAttributes(), 'VideoChannel'), this.buildOpts) | ||
160 | channelModel.Actor = this.buildActor(row, 'VideoChannel') | ||
161 | |||
162 | const accountModel = new AccountModel(this.grab(row, this.tables.getAccountAttributes(), 'VideoChannel.Account'), this.buildOpts) | ||
163 | accountModel.Actor = this.buildActor(row, 'VideoChannel.Account') | ||
164 | |||
165 | channelModel.Account = accountModel | ||
166 | |||
167 | videoModel.VideoChannel = channelModel | ||
168 | } | ||
169 | |||
170 | private buildActor (row: SQLRow, prefix: string) { | ||
171 | const actorPrefix = `${prefix}.Actor` | ||
172 | const avatarPrefix = `${actorPrefix}.Avatar` | ||
173 | const serverPrefix = `${actorPrefix}.Server` | ||
174 | |||
175 | const avatarModel = row[`${avatarPrefix}.id`] !== null | ||
176 | ? new ActorImageModel(this.grab(row, this.tables.getAvatarAttributes(), avatarPrefix), this.buildOpts) | ||
177 | : null | ||
178 | |||
179 | const serverModel = row[`${serverPrefix}.id`] !== null | ||
180 | ? new ServerModel(this.grab(row, this.tables.getServerAttributes(), serverPrefix), this.buildOpts) | ||
181 | : null | ||
182 | |||
183 | const actorModel = new ActorModel(this.grab(row, this.tables.getActorAttributes(), actorPrefix), this.buildOpts) | ||
184 | actorModel.Avatar = avatarModel | ||
185 | actorModel.Server = serverModel | ||
186 | |||
187 | return actorModel | ||
188 | } | ||
189 | |||
190 | private setUserHistory (row: SQLRow, videoModel: VideoModel) { | ||
191 | const id = row['userVideoHistory.id'] | ||
192 | if (!id || this.historyDone.has(id)) return | ||
193 | |||
194 | const attributes = this.grab(row, this.tables.getUserHistoryAttributes(), 'userVideoHistory') | ||
195 | const historyModel = new UserVideoHistoryModel(attributes, this.buildOpts) | ||
196 | videoModel.UserVideoHistories.push(historyModel) | ||
197 | |||
198 | this.historyDone.add(id) | ||
199 | } | ||
200 | |||
201 | private addThumbnail (row: SQLRow, videoModel: VideoModel) { | ||
202 | const id = row['Thumbnails.id'] | ||
203 | if (!id || this.thumbnailsDone.has(id)) return | ||
204 | |||
205 | const attributes = this.grab(row, this.tables.getThumbnailAttributes(), 'Thumbnails') | ||
206 | const thumbnailModel = new ThumbnailModel(attributes, this.buildOpts) | ||
207 | videoModel.Thumbnails.push(thumbnailModel) | ||
208 | |||
209 | this.thumbnailsDone.add(id) | ||
210 | } | ||
211 | |||
212 | private addWebTorrentFile (row: SQLRow, videoModel: VideoModel) { | ||
213 | const id = row['VideoFiles.id'] | ||
214 | if (!id || this.videoFileMemo[id]) return | ||
215 | |||
216 | const attributes = this.grab(row, this.tables.getFileAttributes(), 'VideoFiles') | ||
217 | const videoFileModel = new VideoFileModel(attributes, this.buildOpts) | ||
218 | videoModel.VideoFiles.push(videoFileModel) | ||
219 | |||
220 | this.videoFileMemo[id] = videoFileModel | ||
221 | } | ||
222 | |||
223 | private addStreamingPlaylist (row: SQLRow, videoModel: VideoModel) { | ||
224 | const id = row['VideoStreamingPlaylists.id'] | ||
225 | if (!id || this.videoStreamingPlaylistMemo[id]) return | ||
226 | |||
227 | const attributes = this.grab(row, this.tables.getStreamingPlaylistAttributes(), 'VideoStreamingPlaylists') | ||
228 | const streamingPlaylist = new VideoStreamingPlaylistModel(attributes, this.buildOpts) | ||
229 | streamingPlaylist.VideoFiles = [] | ||
230 | |||
231 | videoModel.VideoStreamingPlaylists.push(streamingPlaylist) | ||
232 | |||
233 | this.videoStreamingPlaylistMemo[id] = streamingPlaylist | ||
234 | } | ||
235 | |||
236 | private addStreamingPlaylistFile (row: SQLRow) { | ||
237 | const id = row['VideoStreamingPlaylists.VideoFiles.id'] | ||
238 | if (!id || this.videoFileMemo[id]) return | ||
239 | |||
240 | const streamingPlaylist = this.videoStreamingPlaylistMemo[row['VideoStreamingPlaylists.id']] | ||
241 | |||
242 | const attributes = this.grab(row, this.tables.getFileAttributes(), 'VideoStreamingPlaylists.VideoFiles') | ||
243 | const videoFileModel = new VideoFileModel(attributes, this.buildOpts) | ||
244 | streamingPlaylist.VideoFiles.push(videoFileModel) | ||
245 | |||
246 | this.videoFileMemo[id] = videoFileModel | ||
247 | } | ||
248 | |||
249 | private addRedundancy (row: SQLRow, prefix: string, to: VideoFileModel | VideoStreamingPlaylistModel) { | ||
250 | if (!to.RedundancyVideos) to.RedundancyVideos = [] | ||
251 | |||
252 | const redundancyPrefix = `${prefix}.RedundancyVideos` | ||
253 | const id = row[`${redundancyPrefix}.id`] | ||
254 | |||
255 | if (!id || this.redundancyDone.has(id)) return | ||
256 | |||
257 | const attributes = this.grab(row, this.tables.getRedundancyAttributes(), redundancyPrefix) | ||
258 | const redundancyModel = new VideoRedundancyModel(attributes, this.buildOpts) | ||
259 | to.RedundancyVideos.push(redundancyModel) | ||
260 | |||
261 | this.redundancyDone.add(id) | ||
262 | } | ||
263 | |||
264 | private addTag (row: SQLRow, videoModel: VideoModel) { | ||
265 | if (!row['Tags.name']) return | ||
266 | |||
267 | const key = `${row['Tags.VideoTagModel.videoId']}-${row['Tags.VideoTagModel.tagId']}` | ||
268 | if (this.tagsDone.has(key)) return | ||
269 | |||
270 | const attributes = this.grab(row, this.tables.getTagAttributes(), 'Tags') | ||
271 | const tagModel = new TagModel(attributes, this.buildOpts) | ||
272 | videoModel.Tags.push(tagModel) | ||
273 | |||
274 | this.tagsDone.add(key) | ||
275 | } | ||
276 | |||
277 | private addTracker (row: SQLRow, videoModel: VideoModel) { | ||
278 | if (!row['Trackers.id']) return | ||
279 | |||
280 | const key = `${row['Trackers.VideoTrackerModel.videoId']}-${row['Trackers.VideoTrackerModel.trackerId']}` | ||
281 | if (this.trackersDone.has(key)) return | ||
282 | |||
283 | const attributes = this.grab(row, this.tables.getTrackerAttributes(), 'Trackers') | ||
284 | const trackerModel = new TrackerModel(attributes, this.buildOpts) | ||
285 | videoModel.Trackers.push(trackerModel) | ||
286 | |||
287 | this.trackersDone.add(key) | ||
288 | } | ||
289 | |||
290 | private setBlacklisted (row: SQLRow, videoModel: VideoModel) { | ||
291 | const id = row['VideoBlacklist.id'] | ||
292 | if (!id || this.blacklistDone.has(id)) return | ||
293 | |||
294 | const attributes = this.grab(row, this.tables.getBlacklistedAttributes(), 'VideoBlacklist') | ||
295 | videoModel.VideoBlacklist = new VideoBlacklistModel(attributes, this.buildOpts) | ||
296 | |||
297 | this.blacklistDone.add(id) | ||
298 | } | ||
299 | |||
300 | private setScheduleVideoUpdate (row: SQLRow, videoModel: VideoModel) { | ||
301 | const id = row['ScheduleVideoUpdate.id'] | ||
302 | if (!id || this.scheduleVideoUpdateDone.has(id)) return | ||
303 | |||
304 | const attributes = this.grab(row, this.tables.getScheduleUpdateAttributes(), 'ScheduleVideoUpdate') | ||
305 | videoModel.ScheduleVideoUpdate = new ScheduleVideoUpdateModel(attributes, this.buildOpts) | ||
306 | |||
307 | this.scheduleVideoUpdateDone.add(id) | ||
308 | } | ||
309 | |||
310 | private setLive (row: SQLRow, videoModel: VideoModel) { | ||
311 | const id = row['VideoLive.id'] | ||
312 | if (!id || this.liveDone.has(id)) return | ||
313 | |||
314 | const attributes = this.grab(row, this.tables.getLiveAttributes(), 'VideoLive') | ||
315 | videoModel.VideoLive = new VideoLiveModel(attributes, this.buildOpts) | ||
316 | |||
317 | this.liveDone.add(id) | ||
318 | } | ||
319 | |||
320 | private grab (row: SQLRow, attributes: string[], prefix: string) { | ||
321 | const result: { [ id: string ]: string | number } = {} | ||
322 | |||
323 | for (const a of attributes) { | ||
324 | const key = prefix | ||
325 | ? prefix + '.' + a | ||
326 | : a | ||
327 | |||
328 | result[a] = row[key] | ||
329 | } | ||
330 | |||
331 | return result | ||
332 | } | ||
333 | } | ||
diff --git a/server/models/video/sql/shared/video-tables.ts b/server/models/video/sql/shared/video-tables.ts new file mode 100644 index 000000000..abdd22188 --- /dev/null +++ b/server/models/video/sql/shared/video-tables.ts | |||
@@ -0,0 +1,263 @@ | |||
1 | |||
2 | /** | ||
3 | * | ||
4 | * Class to build video attributes/join names we want to fetch from the database | ||
5 | * | ||
6 | */ | ||
7 | export class VideoTables { | ||
8 | |||
9 | constructor (readonly mode: 'get' | 'list') { | ||
10 | |||
11 | } | ||
12 | |||
13 | getChannelAttributesForUser () { | ||
14 | return [ 'id', 'accountId' ] | ||
15 | } | ||
16 | |||
17 | getChannelAttributes () { | ||
18 | let attributeKeys = [ | ||
19 | 'id', | ||
20 | 'name', | ||
21 | 'description', | ||
22 | 'actorId' | ||
23 | ] | ||
24 | |||
25 | if (this.mode === 'get') { | ||
26 | attributeKeys = attributeKeys.concat([ | ||
27 | 'support', | ||
28 | 'createdAt', | ||
29 | 'updatedAt' | ||
30 | ]) | ||
31 | } | ||
32 | |||
33 | return attributeKeys | ||
34 | } | ||
35 | |||
36 | getUserAccountAttributes () { | ||
37 | return [ 'id', 'userId' ] | ||
38 | } | ||
39 | |||
40 | getAccountAttributes () { | ||
41 | let attributeKeys = [ 'id', 'name', 'actorId' ] | ||
42 | |||
43 | if (this.mode === 'get') { | ||
44 | attributeKeys = attributeKeys.concat([ | ||
45 | 'description', | ||
46 | 'userId', | ||
47 | 'createdAt', | ||
48 | 'updatedAt' | ||
49 | ]) | ||
50 | } | ||
51 | |||
52 | return attributeKeys | ||
53 | } | ||
54 | |||
55 | getThumbnailAttributes () { | ||
56 | let attributeKeys = [ 'id', 'type', 'filename' ] | ||
57 | |||
58 | if (this.mode === 'get') { | ||
59 | attributeKeys = attributeKeys.concat([ | ||
60 | 'height', | ||
61 | 'width', | ||
62 | 'fileUrl', | ||
63 | 'automaticallyGenerated', | ||
64 | 'videoId', | ||
65 | 'videoPlaylistId', | ||
66 | 'createdAt', | ||
67 | 'updatedAt' | ||
68 | ]) | ||
69 | } | ||
70 | |||
71 | return attributeKeys | ||
72 | } | ||
73 | |||
74 | getFileAttributes () { | ||
75 | return [ | ||
76 | 'id', | ||
77 | 'createdAt', | ||
78 | 'updatedAt', | ||
79 | 'resolution', | ||
80 | 'size', | ||
81 | 'extname', | ||
82 | 'filename', | ||
83 | 'fileUrl', | ||
84 | 'torrentFilename', | ||
85 | 'torrentUrl', | ||
86 | 'infoHash', | ||
87 | 'fps', | ||
88 | 'metadataUrl', | ||
89 | 'videoStreamingPlaylistId', | ||
90 | 'videoId' | ||
91 | ] | ||
92 | } | ||
93 | |||
94 | getStreamingPlaylistAttributes () { | ||
95 | let playlistKeys = [ 'id', 'playlistUrl', 'type' ] | ||
96 | |||
97 | if (this.mode === 'get') { | ||
98 | playlistKeys = playlistKeys.concat([ | ||
99 | 'p2pMediaLoaderInfohashes', | ||
100 | 'p2pMediaLoaderPeerVersion', | ||
101 | 'segmentsSha256Url', | ||
102 | 'videoId', | ||
103 | 'createdAt', | ||
104 | 'updatedAt' | ||
105 | ]) | ||
106 | } | ||
107 | |||
108 | return playlistKeys | ||
109 | } | ||
110 | |||
111 | getUserHistoryAttributes () { | ||
112 | return [ 'id', 'currentTime' ] | ||
113 | } | ||
114 | |||
115 | getPlaylistAttributes () { | ||
116 | return [ | ||
117 | 'createdAt', | ||
118 | 'updatedAt', | ||
119 | 'url', | ||
120 | 'position', | ||
121 | 'startTimestamp', | ||
122 | 'stopTimestamp', | ||
123 | 'videoPlaylistId' | ||
124 | ] | ||
125 | } | ||
126 | |||
127 | getTagAttributes () { | ||
128 | return [ 'id', 'name' ] | ||
129 | } | ||
130 | |||
131 | getVideoTagAttributes () { | ||
132 | return [ 'videoId', 'tagId', 'createdAt', 'updatedAt' ] | ||
133 | } | ||
134 | |||
135 | getBlacklistedAttributes () { | ||
136 | return [ 'id', 'reason', 'unfederated' ] | ||
137 | } | ||
138 | |||
139 | getScheduleUpdateAttributes () { | ||
140 | return [ | ||
141 | 'id', | ||
142 | 'updateAt', | ||
143 | 'privacy', | ||
144 | 'videoId', | ||
145 | 'createdAt', | ||
146 | 'updatedAt' | ||
147 | ] | ||
148 | } | ||
149 | |||
150 | getLiveAttributes () { | ||
151 | return [ | ||
152 | 'id', | ||
153 | 'streamKey', | ||
154 | 'saveReplay', | ||
155 | 'permanentLive', | ||
156 | 'videoId', | ||
157 | 'createdAt', | ||
158 | 'updatedAt' | ||
159 | ] | ||
160 | } | ||
161 | |||
162 | getTrackerAttributes () { | ||
163 | return [ 'id', 'url' ] | ||
164 | } | ||
165 | |||
166 | getVideoTrackerAttributes () { | ||
167 | return [ | ||
168 | 'videoId', | ||
169 | 'trackerId', | ||
170 | 'createdAt', | ||
171 | 'updatedAt' | ||
172 | ] | ||
173 | } | ||
174 | |||
175 | getRedundancyAttributes () { | ||
176 | return [ 'id', 'fileUrl' ] | ||
177 | } | ||
178 | |||
179 | getActorAttributes () { | ||
180 | let attributeKeys = [ | ||
181 | 'id', | ||
182 | 'preferredUsername', | ||
183 | 'url', | ||
184 | 'serverId', | ||
185 | 'avatarId' | ||
186 | ] | ||
187 | |||
188 | if (this.mode === 'get') { | ||
189 | attributeKeys = attributeKeys.concat([ | ||
190 | 'type', | ||
191 | 'followersCount', | ||
192 | 'followingCount', | ||
193 | 'inboxUrl', | ||
194 | 'outboxUrl', | ||
195 | 'sharedInboxUrl', | ||
196 | 'followersUrl', | ||
197 | 'followingUrl', | ||
198 | 'remoteCreatedAt', | ||
199 | 'createdAt', | ||
200 | 'updatedAt' | ||
201 | ]) | ||
202 | } | ||
203 | |||
204 | return attributeKeys | ||
205 | } | ||
206 | |||
207 | getAvatarAttributes () { | ||
208 | let attributeKeys = [ | ||
209 | 'id', | ||
210 | 'filename', | ||
211 | 'type', | ||
212 | 'fileUrl', | ||
213 | 'onDisk', | ||
214 | 'createdAt', | ||
215 | 'updatedAt' | ||
216 | ] | ||
217 | |||
218 | if (this.mode === 'get') { | ||
219 | attributeKeys = attributeKeys.concat([ | ||
220 | 'height', | ||
221 | 'width', | ||
222 | 'type' | ||
223 | ]) | ||
224 | } | ||
225 | |||
226 | return attributeKeys | ||
227 | } | ||
228 | |||
229 | getServerAttributes () { | ||
230 | return [ 'id', 'host' ] | ||
231 | } | ||
232 | |||
233 | getVideoAttributes () { | ||
234 | return [ | ||
235 | 'id', | ||
236 | 'uuid', | ||
237 | 'name', | ||
238 | 'category', | ||
239 | 'licence', | ||
240 | 'language', | ||
241 | 'privacy', | ||
242 | 'nsfw', | ||
243 | 'description', | ||
244 | 'support', | ||
245 | 'duration', | ||
246 | 'views', | ||
247 | 'likes', | ||
248 | 'dislikes', | ||
249 | 'remote', | ||
250 | 'isLive', | ||
251 | 'url', | ||
252 | 'commentsEnabled', | ||
253 | 'downloadEnabled', | ||
254 | 'waitTranscoding', | ||
255 | 'state', | ||
256 | 'publishedAt', | ||
257 | 'originallyPublishedAt', | ||
258 | 'channelId', | ||
259 | 'createdAt', | ||
260 | 'updatedAt' | ||
261 | ] | ||
262 | } | ||
263 | } | ||
diff --git a/server/models/video/sql/video-model-get-query-builder.ts b/server/models/video/sql/video-model-get-query-builder.ts new file mode 100644 index 000000000..f234e8778 --- /dev/null +++ b/server/models/video/sql/video-model-get-query-builder.ts | |||
@@ -0,0 +1,173 @@ | |||
1 | import { Sequelize, Transaction } from 'sequelize' | ||
2 | import { AbstractVideosModelQueryBuilder } from './shared/abstract-videos-model-query-builder' | ||
3 | import { VideoFileQueryBuilder } from './shared/video-file-query-builder' | ||
4 | import { VideoModelBuilder } from './shared/video-model-builder' | ||
5 | import { VideoTables } from './shared/video-tables' | ||
6 | |||
7 | /** | ||
8 | * | ||
9 | * Build a GET SQL query, fetch rows and create the video model | ||
10 | * | ||
11 | */ | ||
12 | |||
13 | export type GetType = | ||
14 | 'api' | | ||
15 | 'full-light' | | ||
16 | 'account-blacklist-files' | | ||
17 | 'all-files' | | ||
18 | 'thumbnails' | | ||
19 | 'thumbnails-blacklist' | | ||
20 | 'id' | | ||
21 | 'blacklist-rights' | ||
22 | |||
23 | export type BuildVideoGetQueryOptions = { | ||
24 | id?: number | string | ||
25 | url?: string | ||
26 | |||
27 | type: GetType | ||
28 | |||
29 | userId?: number | ||
30 | transaction?: Transaction | ||
31 | |||
32 | logging?: boolean | ||
33 | } | ||
34 | |||
35 | export class VideosModelGetQueryBuilder { | ||
36 | videoQueryBuilder: VideosModelGetQuerySubBuilder | ||
37 | webtorrentFilesQueryBuilder: VideoFileQueryBuilder | ||
38 | streamingPlaylistFilesQueryBuilder: VideoFileQueryBuilder | ||
39 | |||
40 | private readonly videoModelBuilder: VideoModelBuilder | ||
41 | |||
42 | private static readonly videoFilesInclude = new Set<GetType>([ 'api', 'full-light', 'account-blacklist-files', 'all-files' ]) | ||
43 | |||
44 | constructor (protected readonly sequelize: Sequelize) { | ||
45 | this.videoQueryBuilder = new VideosModelGetQuerySubBuilder(sequelize) | ||
46 | this.webtorrentFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) | ||
47 | this.streamingPlaylistFilesQueryBuilder = new VideoFileQueryBuilder(sequelize) | ||
48 | |||
49 | this.videoModelBuilder = new VideoModelBuilder('get', new VideoTables('get')) | ||
50 | } | ||
51 | |||
52 | async queryVideo (options: BuildVideoGetQueryOptions) { | ||
53 | const [ videoRows, webtorrentFilesRows, streamingPlaylistFilesRows ] = await Promise.all([ | ||
54 | this.videoQueryBuilder.queryVideos(options), | ||
55 | |||
56 | VideosModelGetQueryBuilder.videoFilesInclude.has(options.type) | ||
57 | ? this.webtorrentFilesQueryBuilder.queryWebTorrentVideos(options) | ||
58 | : Promise.resolve(undefined), | ||
59 | |||
60 | VideosModelGetQueryBuilder.videoFilesInclude.has(options.type) | ||
61 | ? this.streamingPlaylistFilesQueryBuilder.queryStreamingPlaylistVideos(options) | ||
62 | : Promise.resolve(undefined) | ||
63 | ]) | ||
64 | |||
65 | const videos = this.videoModelBuilder.buildVideosFromRows(videoRows, webtorrentFilesRows, streamingPlaylistFilesRows) | ||
66 | |||
67 | if (videos.length > 1) { | ||
68 | throw new Error('Video results is more than ') | ||
69 | } | ||
70 | |||
71 | if (videos.length === 0) return null | ||
72 | return videos[0] | ||
73 | } | ||
74 | } | ||
75 | |||
76 | export class VideosModelGetQuerySubBuilder extends AbstractVideosModelQueryBuilder { | ||
77 | protected attributes: { [key: string]: string } | ||
78 | |||
79 | protected webtorrentFilesQuery: string | ||
80 | protected streamingPlaylistFilesQuery: string | ||
81 | |||
82 | private static readonly trackersInclude = new Set<GetType>([ 'api' ]) | ||
83 | private static readonly liveInclude = new Set<GetType>([ 'api', 'full-light' ]) | ||
84 | private static readonly scheduleUpdateInclude = new Set<GetType>([ 'api', 'full-light' ]) | ||
85 | private static readonly tagsInclude = new Set<GetType>([ 'api', 'full-light' ]) | ||
86 | private static readonly userHistoryInclude = new Set<GetType>([ 'api', 'full-light' ]) | ||
87 | private static readonly accountInclude = new Set<GetType>([ 'api', 'full-light', 'account-blacklist-files' ]) | ||
88 | private static readonly ownerUserInclude = new Set<GetType>([ 'blacklist-rights' ]) | ||
89 | |||
90 | private static readonly blacklistedInclude = new Set<GetType>([ | ||
91 | 'api', | ||
92 | 'full-light', | ||
93 | 'account-blacklist-files', | ||
94 | 'thumbnails-blacklist', | ||
95 | 'blacklist-rights' | ||
96 | ]) | ||
97 | |||
98 | private static readonly thumbnailsInclude = new Set<GetType>([ | ||
99 | 'api', | ||
100 | 'full-light', | ||
101 | 'account-blacklist-files', | ||
102 | 'all-files', | ||
103 | 'thumbnails', | ||
104 | 'thumbnails-blacklist' | ||
105 | ]) | ||
106 | |||
107 | constructor (protected readonly sequelize: Sequelize) { | ||
108 | super('get') | ||
109 | } | ||
110 | |||
111 | queryVideos (options: BuildVideoGetQueryOptions) { | ||
112 | this.buildMainGetQuery(options) | ||
113 | |||
114 | return this.runQuery(options) | ||
115 | } | ||
116 | |||
117 | private buildMainGetQuery (options: BuildVideoGetQueryOptions) { | ||
118 | this.attributes = { | ||
119 | '"video".*': '' | ||
120 | } | ||
121 | |||
122 | if (VideosModelGetQuerySubBuilder.thumbnailsInclude.has(options.type)) { | ||
123 | this.includeThumbnails() | ||
124 | } | ||
125 | |||
126 | if (VideosModelGetQuerySubBuilder.blacklistedInclude.has(options.type)) { | ||
127 | this.includeBlacklisted() | ||
128 | } | ||
129 | |||
130 | if (VideosModelGetQuerySubBuilder.accountInclude.has(options.type)) { | ||
131 | this.includeChannels() | ||
132 | this.includeAccounts() | ||
133 | } | ||
134 | |||
135 | if (VideosModelGetQuerySubBuilder.tagsInclude.has(options.type)) { | ||
136 | this.includeTags() | ||
137 | } | ||
138 | |||
139 | if (VideosModelGetQuerySubBuilder.scheduleUpdateInclude.has(options.type)) { | ||
140 | this.includeScheduleUpdate() | ||
141 | } | ||
142 | |||
143 | if (VideosModelGetQuerySubBuilder.liveInclude.has(options.type)) { | ||
144 | this.includeLive() | ||
145 | } | ||
146 | |||
147 | if (options.userId && VideosModelGetQuerySubBuilder.userHistoryInclude.has(options.type)) { | ||
148 | this.includeUserHistory(options.userId) | ||
149 | } | ||
150 | |||
151 | if (VideosModelGetQuerySubBuilder.ownerUserInclude.has(options.type)) { | ||
152 | this.includeOwnerUser() | ||
153 | } | ||
154 | |||
155 | if (VideosModelGetQuerySubBuilder.trackersInclude.has(options.type)) { | ||
156 | this.includeTrackers() | ||
157 | } | ||
158 | |||
159 | this.whereId(options) | ||
160 | |||
161 | this.query = this.buildQuery(options) | ||
162 | } | ||
163 | |||
164 | private buildQuery (options: BuildVideoGetQueryOptions) { | ||
165 | const order = VideosModelGetQuerySubBuilder.tagsInclude.has(options.type) | ||
166 | ? 'ORDER BY "Tags"."name" ASC' | ||
167 | : '' | ||
168 | |||
169 | const from = `SELECT * FROM "video" ${this.where} LIMIT 1` | ||
170 | |||
171 | return `${this.buildSelect()} FROM (${from}) AS "video" ${this.joins} ${order}` | ||
172 | } | ||
173 | } | ||
diff --git a/server/models/video/sql/videos-id-list-query-builder.ts b/server/models/video/sql/videos-id-list-query-builder.ts new file mode 100644 index 000000000..30b251f0f --- /dev/null +++ b/server/models/video/sql/videos-id-list-query-builder.ts | |||
@@ -0,0 +1,616 @@ | |||
1 | import { Sequelize } from 'sequelize' | ||
2 | import validator from 'validator' | ||
3 | import { exists } from '@server/helpers/custom-validators/misc' | ||
4 | import { buildDirectionAndField, createSafeIn } from '@server/models/utils' | ||
5 | import { MUserAccountId, MUserId } from '@server/types/models' | ||
6 | import { VideoFilter, VideoPrivacy, VideoState } from '@shared/models' | ||
7 | import { AbstractVideosQueryBuilder } from './shared/abstract-videos-query-builder' | ||
8 | |||
9 | /** | ||
10 | * | ||
11 | * Build videos list SQL query to fetch rows | ||
12 | * | ||
13 | */ | ||
14 | |||
15 | export type BuildVideosListQueryOptions = { | ||
16 | attributes?: string[] | ||
17 | |||
18 | serverAccountId: number | ||
19 | followerActorId: number | ||
20 | includeLocalVideos: boolean | ||
21 | |||
22 | count: number | ||
23 | start: number | ||
24 | sort: string | ||
25 | |||
26 | nsfw?: boolean | ||
27 | filter?: VideoFilter | ||
28 | isLive?: boolean | ||
29 | |||
30 | categoryOneOf?: number[] | ||
31 | licenceOneOf?: number[] | ||
32 | languageOneOf?: string[] | ||
33 | tagsOneOf?: string[] | ||
34 | tagsAllOf?: string[] | ||
35 | |||
36 | withFiles?: boolean | ||
37 | |||
38 | accountId?: number | ||
39 | videoChannelId?: number | ||
40 | |||
41 | videoPlaylistId?: number | ||
42 | |||
43 | trendingAlgorithm?: string // best, hot, or any other algorithm implemented | ||
44 | trendingDays?: number | ||
45 | |||
46 | user?: MUserAccountId | ||
47 | historyOfUser?: MUserId | ||
48 | |||
49 | startDate?: string // ISO 8601 | ||
50 | endDate?: string // ISO 8601 | ||
51 | originallyPublishedStartDate?: string | ||
52 | originallyPublishedEndDate?: string | ||
53 | |||
54 | durationMin?: number // seconds | ||
55 | durationMax?: number // seconds | ||
56 | |||
57 | search?: string | ||
58 | |||
59 | isCount?: boolean | ||
60 | |||
61 | group?: string | ||
62 | having?: string | ||
63 | } | ||
64 | |||
65 | export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder { | ||
66 | protected replacements: any = {} | ||
67 | |||
68 | private attributes: string[] | ||
69 | private joins: string[] = [] | ||
70 | |||
71 | private readonly and: string[] = [] | ||
72 | |||
73 | private readonly cte: string[] = [] | ||
74 | |||
75 | private group = '' | ||
76 | private having = '' | ||
77 | |||
78 | private sort = '' | ||
79 | private limit = '' | ||
80 | private offset = '' | ||
81 | |||
82 | constructor (protected readonly sequelize: Sequelize) { | ||
83 | super() | ||
84 | } | ||
85 | |||
86 | queryVideoIds (options: BuildVideosListQueryOptions) { | ||
87 | this.buildIdsListQuery(options) | ||
88 | |||
89 | return this.runQuery() | ||
90 | } | ||
91 | |||
92 | countVideoIds (countOptions: BuildVideosListQueryOptions): Promise<number> { | ||
93 | this.buildIdsListQuery(countOptions) | ||
94 | |||
95 | return this.runQuery().then(rows => rows.length !== 0 ? rows[0].total : 0) | ||
96 | } | ||
97 | |||
98 | getIdsListQueryAndSort (options: BuildVideosListQueryOptions) { | ||
99 | this.buildIdsListQuery(options) | ||
100 | return { query: this.query, sort: this.sort, replacements: this.replacements } | ||
101 | } | ||
102 | |||
103 | private buildIdsListQuery (options: BuildVideosListQueryOptions) { | ||
104 | this.attributes = options.attributes || [ '"video"."id"' ] | ||
105 | |||
106 | if (options.group) this.group = options.group | ||
107 | if (options.having) this.having = options.having | ||
108 | |||
109 | this.joins = this.joins.concat([ | ||
110 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId"', | ||
111 | 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId"', | ||
112 | 'INNER JOIN "actor" "accountActor" ON "account"."actorId" = "accountActor"."id"' | ||
113 | ]) | ||
114 | |||
115 | this.whereNotBlacklisted() | ||
116 | |||
117 | if (options.serverAccountId) { | ||
118 | this.whereNotBlocked(options.serverAccountId, options.user) | ||
119 | } | ||
120 | |||
121 | // Only list public/published videos | ||
122 | if (!options.filter || (options.filter !== 'all-local' && options.filter !== 'all')) { | ||
123 | this.whereStateAndPrivacyAvailable(options.user) | ||
124 | } | ||
125 | |||
126 | if (options.videoPlaylistId) { | ||
127 | this.joinPlaylist(options.videoPlaylistId) | ||
128 | } | ||
129 | |||
130 | if (options.filter && (options.filter === 'local' || options.filter === 'all-local')) { | ||
131 | this.whereOnlyLocal() | ||
132 | } | ||
133 | |||
134 | if (options.accountId) { | ||
135 | this.whereAccountId(options.accountId) | ||
136 | } | ||
137 | |||
138 | if (options.videoChannelId) { | ||
139 | this.whereChannelId(options.videoChannelId) | ||
140 | } | ||
141 | |||
142 | if (options.followerActorId) { | ||
143 | this.whereFollowerActorId(options.followerActorId, options.includeLocalVideos) | ||
144 | } | ||
145 | |||
146 | if (options.withFiles === true) { | ||
147 | this.whereFileExists() | ||
148 | } | ||
149 | |||
150 | if (options.tagsOneOf) { | ||
151 | this.whereTagsOneOf(options.tagsOneOf) | ||
152 | } | ||
153 | |||
154 | if (options.tagsAllOf) { | ||
155 | this.whereTagsAllOf(options.tagsAllOf) | ||
156 | } | ||
157 | |||
158 | if (options.nsfw === true) { | ||
159 | this.whereNSFW() | ||
160 | } else if (options.nsfw === false) { | ||
161 | this.whereSFW() | ||
162 | } | ||
163 | |||
164 | if (options.isLive === true) { | ||
165 | this.whereLive() | ||
166 | } else if (options.isLive === false) { | ||
167 | this.whereVOD() | ||
168 | } | ||
169 | |||
170 | if (options.categoryOneOf) { | ||
171 | this.whereCategoryOneOf(options.categoryOneOf) | ||
172 | } | ||
173 | |||
174 | if (options.licenceOneOf) { | ||
175 | this.whereLicenceOneOf(options.licenceOneOf) | ||
176 | } | ||
177 | |||
178 | if (options.languageOneOf) { | ||
179 | this.whereLanguageOneOf(options.languageOneOf) | ||
180 | } | ||
181 | |||
182 | // We don't exclude results in this so if we do a count we don't need to add this complex clause | ||
183 | if (options.isCount !== true) { | ||
184 | if (options.trendingDays) { | ||
185 | this.groupForTrending(options.trendingDays) | ||
186 | } else if ([ 'best', 'hot' ].includes(options.trendingAlgorithm)) { | ||
187 | this.groupForHotOrBest(options.trendingAlgorithm, options.user) | ||
188 | } | ||
189 | } | ||
190 | |||
191 | if (options.historyOfUser) { | ||
192 | this.joinHistory(options.historyOfUser.id) | ||
193 | } | ||
194 | |||
195 | if (options.startDate) { | ||
196 | this.whereStartDate(options.startDate) | ||
197 | } | ||
198 | |||
199 | if (options.endDate) { | ||
200 | this.whereEndDate(options.endDate) | ||
201 | } | ||
202 | |||
203 | if (options.originallyPublishedStartDate) { | ||
204 | this.whereOriginallyPublishedStartDate(options.originallyPublishedStartDate) | ||
205 | } | ||
206 | |||
207 | if (options.originallyPublishedEndDate) { | ||
208 | this.whereOriginallyPublishedEndDate(options.originallyPublishedEndDate) | ||
209 | } | ||
210 | |||
211 | if (options.durationMin) { | ||
212 | this.whereDurationMin(options.durationMin) | ||
213 | } | ||
214 | |||
215 | if (options.durationMax) { | ||
216 | this.whereDurationMax(options.durationMax) | ||
217 | } | ||
218 | |||
219 | this.whereSearch(options.search) | ||
220 | |||
221 | if (options.isCount === true) { | ||
222 | this.setCountAttribute() | ||
223 | } else { | ||
224 | if (exists(options.sort)) { | ||
225 | this.setSort(options.sort) | ||
226 | } | ||
227 | |||
228 | if (exists(options.count)) { | ||
229 | this.setLimit(options.count) | ||
230 | } | ||
231 | |||
232 | if (exists(options.start)) { | ||
233 | this.setOffset(options.start) | ||
234 | } | ||
235 | } | ||
236 | |||
237 | const cteString = this.cte.length !== 0 | ||
238 | ? `WITH ${this.cte.join(', ')} ` | ||
239 | : '' | ||
240 | |||
241 | this.query = cteString + | ||
242 | 'SELECT ' + this.attributes.join(', ') + ' ' + | ||
243 | 'FROM "video" ' + this.joins.join(' ') + ' ' + | ||
244 | 'WHERE ' + this.and.join(' AND ') + ' ' + | ||
245 | this.group + ' ' + | ||
246 | this.having + ' ' + | ||
247 | this.sort + ' ' + | ||
248 | this.limit + ' ' + | ||
249 | this.offset | ||
250 | } | ||
251 | |||
252 | private setCountAttribute () { | ||
253 | this.attributes = [ 'COUNT(*) as "total"' ] | ||
254 | } | ||
255 | |||
256 | private joinHistory (userId: number) { | ||
257 | this.joins.push('INNER JOIN "userVideoHistory" ON "video"."id" = "userVideoHistory"."videoId"') | ||
258 | |||
259 | this.and.push('"userVideoHistory"."userId" = :historyOfUser') | ||
260 | |||
261 | this.replacements.historyOfUser = userId | ||
262 | } | ||
263 | |||
264 | private joinPlaylist (playlistId: number) { | ||
265 | this.joins.push( | ||
266 | 'INNER JOIN "videoPlaylistElement" "video"."id" = "videoPlaylistElement"."videoId" ' + | ||
267 | 'AND "videoPlaylistElement"."videoPlaylistId" = :videoPlaylistId' | ||
268 | ) | ||
269 | |||
270 | this.replacements.videoPlaylistId = playlistId | ||
271 | } | ||
272 | |||
273 | private whereStateAndPrivacyAvailable (user?: MUserAccountId) { | ||
274 | this.and.push( | ||
275 | `("video"."state" = ${VideoState.PUBLISHED} OR ` + | ||
276 | `("video"."state" = ${VideoState.TO_TRANSCODE} AND "video"."waitTranscoding" IS false))` | ||
277 | ) | ||
278 | |||
279 | if (user) { | ||
280 | this.and.push( | ||
281 | `("video"."privacy" = ${VideoPrivacy.PUBLIC} OR "video"."privacy" = ${VideoPrivacy.INTERNAL})` | ||
282 | ) | ||
283 | } else { // Or only public videos | ||
284 | this.and.push( | ||
285 | `"video"."privacy" = ${VideoPrivacy.PUBLIC}` | ||
286 | ) | ||
287 | } | ||
288 | } | ||
289 | |||
290 | private whereOnlyLocal () { | ||
291 | this.and.push('"video"."remote" IS FALSE') | ||
292 | } | ||
293 | |||
294 | private whereAccountId (accountId: number) { | ||
295 | this.and.push('"account"."id" = :accountId') | ||
296 | this.replacements.accountId = accountId | ||
297 | } | ||
298 | |||
299 | private whereChannelId (channelId: number) { | ||
300 | this.and.push('"videoChannel"."id" = :videoChannelId') | ||
301 | this.replacements.videoChannelId = channelId | ||
302 | } | ||
303 | |||
304 | private whereFollowerActorId (followerActorId: number, includeLocalVideos: boolean) { | ||
305 | let query = | ||
306 | '(' + | ||
307 | ' EXISTS (' + | ||
308 | ' SELECT 1 FROM "videoShare" ' + | ||
309 | ' INNER JOIN "actorFollow" "actorFollowShare" ON "actorFollowShare"."targetActorId" = "videoShare"."actorId" ' + | ||
310 | ' AND "actorFollowShare"."actorId" = :followerActorId AND "actorFollowShare"."state" = \'accepted\' ' + | ||
311 | ' WHERE "videoShare"."videoId" = "video"."id"' + | ||
312 | ' )' + | ||
313 | ' OR' + | ||
314 | ' EXISTS (' + | ||
315 | ' SELECT 1 from "actorFollow" ' + | ||
316 | ' WHERE "actorFollow"."targetActorId" = "videoChannel"."actorId" AND "actorFollow"."actorId" = :followerActorId ' + | ||
317 | ' AND "actorFollow"."state" = \'accepted\'' + | ||
318 | ' )' | ||
319 | |||
320 | if (includeLocalVideos) { | ||
321 | query += ' OR "video"."remote" IS FALSE' | ||
322 | } | ||
323 | |||
324 | query += ')' | ||
325 | |||
326 | this.and.push(query) | ||
327 | this.replacements.followerActorId = followerActorId | ||
328 | } | ||
329 | |||
330 | private whereFileExists () { | ||
331 | this.and.push( | ||
332 | '(' + | ||
333 | ' EXISTS (SELECT 1 FROM "videoFile" WHERE "videoFile"."videoId" = "video"."id") ' + | ||
334 | ' OR EXISTS (' + | ||
335 | ' SELECT 1 FROM "videoStreamingPlaylist" ' + | ||
336 | ' INNER JOIN "videoFile" ON "videoFile"."videoStreamingPlaylistId" = "videoStreamingPlaylist"."id" ' + | ||
337 | ' WHERE "videoStreamingPlaylist"."videoId" = "video"."id"' + | ||
338 | ' )' + | ||
339 | ')' | ||
340 | ) | ||
341 | } | ||
342 | |||
343 | private whereTagsOneOf (tagsOneOf: string[]) { | ||
344 | const tagsOneOfLower = tagsOneOf.map(t => t.toLowerCase()) | ||
345 | |||
346 | this.and.push( | ||
347 | 'EXISTS (' + | ||
348 | ' SELECT 1 FROM "videoTag" ' + | ||
349 | ' INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + | ||
350 | ' WHERE lower("tag"."name") IN (' + createSafeIn(this.sequelize, tagsOneOfLower) + ') ' + | ||
351 | ' AND "video"."id" = "videoTag"."videoId"' + | ||
352 | ')' | ||
353 | ) | ||
354 | } | ||
355 | |||
356 | private whereTagsAllOf (tagsAllOf: string[]) { | ||
357 | const tagsAllOfLower = tagsAllOf.map(t => t.toLowerCase()) | ||
358 | |||
359 | this.and.push( | ||
360 | 'EXISTS (' + | ||
361 | ' SELECT 1 FROM "videoTag" ' + | ||
362 | ' INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + | ||
363 | ' WHERE lower("tag"."name") IN (' + createSafeIn(this.sequelize, tagsAllOfLower) + ') ' + | ||
364 | ' AND "video"."id" = "videoTag"."videoId" ' + | ||
365 | ' GROUP BY "videoTag"."videoId" HAVING COUNT(*) = ' + tagsAllOfLower.length + | ||
366 | ')' | ||
367 | ) | ||
368 | } | ||
369 | |||
370 | private whereCategoryOneOf (categoryOneOf: number[]) { | ||
371 | this.and.push('"video"."category" IN (:categoryOneOf)') | ||
372 | this.replacements.categoryOneOf = categoryOneOf | ||
373 | } | ||
374 | |||
375 | private whereLicenceOneOf (licenceOneOf: number[]) { | ||
376 | this.and.push('"video"."licence" IN (:licenceOneOf)') | ||
377 | this.replacements.licenceOneOf = licenceOneOf | ||
378 | } | ||
379 | |||
380 | private whereLanguageOneOf (languageOneOf: string[]) { | ||
381 | const languages = languageOneOf.filter(l => l && l !== '_unknown') | ||
382 | const languagesQueryParts: string[] = [] | ||
383 | |||
384 | if (languages.length !== 0) { | ||
385 | languagesQueryParts.push('"video"."language" IN (:languageOneOf)') | ||
386 | this.replacements.languageOneOf = languages | ||
387 | |||
388 | languagesQueryParts.push( | ||
389 | 'EXISTS (' + | ||
390 | ' SELECT 1 FROM "videoCaption" WHERE "videoCaption"."language" ' + | ||
391 | ' IN (' + createSafeIn(this.sequelize, languages) + ') AND ' + | ||
392 | ' "videoCaption"."videoId" = "video"."id"' + | ||
393 | ')' | ||
394 | ) | ||
395 | } | ||
396 | |||
397 | if (languageOneOf.includes('_unknown')) { | ||
398 | languagesQueryParts.push('"video"."language" IS NULL') | ||
399 | } | ||
400 | |||
401 | if (languagesQueryParts.length !== 0) { | ||
402 | this.and.push('(' + languagesQueryParts.join(' OR ') + ')') | ||
403 | } | ||
404 | } | ||
405 | |||
406 | private whereNSFW () { | ||
407 | this.and.push('"video"."nsfw" IS TRUE') | ||
408 | } | ||
409 | |||
410 | private whereSFW () { | ||
411 | this.and.push('"video"."nsfw" IS FALSE') | ||
412 | } | ||
413 | |||
414 | private whereLive () { | ||
415 | this.and.push('"video"."isLive" IS TRUE') | ||
416 | } | ||
417 | |||
418 | private whereVOD () { | ||
419 | this.and.push('"video"."isLive" IS FALSE') | ||
420 | } | ||
421 | |||
422 | private whereNotBlocked (serverAccountId: number, user?: MUserAccountId) { | ||
423 | const blockerIds = [ serverAccountId ] | ||
424 | if (user) blockerIds.push(user.Account.id) | ||
425 | |||
426 | const inClause = createSafeIn(this.sequelize, blockerIds) | ||
427 | |||
428 | this.and.push( | ||
429 | 'NOT EXISTS (' + | ||
430 | ' SELECT 1 FROM "accountBlocklist" ' + | ||
431 | ' WHERE "accountBlocklist"."accountId" IN (' + inClause + ') ' + | ||
432 | ' AND "accountBlocklist"."targetAccountId" = "account"."id" ' + | ||
433 | ')' + | ||
434 | 'AND NOT EXISTS (' + | ||
435 | ' SELECT 1 FROM "serverBlocklist" WHERE "serverBlocklist"."accountId" IN (' + inClause + ') ' + | ||
436 | ' AND "serverBlocklist"."targetServerId" = "accountActor"."serverId"' + | ||
437 | ')' | ||
438 | ) | ||
439 | } | ||
440 | |||
441 | private whereSearch (search?: string) { | ||
442 | if (!search) { | ||
443 | this.attributes.push('0 as similarity') | ||
444 | return | ||
445 | } | ||
446 | |||
447 | const escapedSearch = this.sequelize.escape(search) | ||
448 | const escapedLikeSearch = this.sequelize.escape('%' + search + '%') | ||
449 | |||
450 | this.cte.push( | ||
451 | '"trigramSearch" AS (' + | ||
452 | ' SELECT "video"."id", ' + | ||
453 | ` similarity(lower(immutable_unaccent("video"."name")), lower(immutable_unaccent(${escapedSearch}))) as similarity ` + | ||
454 | ' FROM "video" ' + | ||
455 | ' WHERE lower(immutable_unaccent("video"."name")) % lower(immutable_unaccent(' + escapedSearch + ')) OR ' + | ||
456 | ' lower(immutable_unaccent("video"."name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))' + | ||
457 | ')' | ||
458 | ) | ||
459 | |||
460 | this.joins.push('LEFT JOIN "trigramSearch" ON "video"."id" = "trigramSearch"."id"') | ||
461 | |||
462 | let base = '(' + | ||
463 | ' "trigramSearch"."id" IS NOT NULL OR ' + | ||
464 | ' EXISTS (' + | ||
465 | ' SELECT 1 FROM "videoTag" ' + | ||
466 | ' INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + | ||
467 | ` WHERE lower("tag"."name") = ${escapedSearch} ` + | ||
468 | ' AND "video"."id" = "videoTag"."videoId"' + | ||
469 | ' )' | ||
470 | |||
471 | if (validator.isUUID(search)) { | ||
472 | base += ` OR "video"."uuid" = ${escapedSearch}` | ||
473 | } | ||
474 | |||
475 | base += ')' | ||
476 | |||
477 | this.and.push(base) | ||
478 | this.attributes.push(`COALESCE("trigramSearch"."similarity", 0) as similarity`) | ||
479 | } | ||
480 | |||
481 | private whereNotBlacklisted () { | ||
482 | this.and.push('"video"."id" NOT IN (SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")') | ||
483 | } | ||
484 | |||
485 | private whereStartDate (startDate: string) { | ||
486 | this.and.push('"video"."publishedAt" >= :startDate') | ||
487 | this.replacements.startDate = startDate | ||
488 | } | ||
489 | |||
490 | private whereEndDate (endDate: string) { | ||
491 | this.and.push('"video"."publishedAt" <= :endDate') | ||
492 | this.replacements.endDate = endDate | ||
493 | } | ||
494 | |||
495 | private whereOriginallyPublishedStartDate (startDate: string) { | ||
496 | this.and.push('"video"."originallyPublishedAt" >= :originallyPublishedStartDate') | ||
497 | this.replacements.originallyPublishedStartDate = startDate | ||
498 | } | ||
499 | |||
500 | private whereOriginallyPublishedEndDate (endDate: string) { | ||
501 | this.and.push('"video"."originallyPublishedAt" <= :originallyPublishedEndDate') | ||
502 | this.replacements.originallyPublishedEndDate = endDate | ||
503 | } | ||
504 | |||
505 | private whereDurationMin (durationMin: number) { | ||
506 | this.and.push('"video"."duration" >= :durationMin') | ||
507 | this.replacements.durationMin = durationMin | ||
508 | } | ||
509 | |||
510 | private whereDurationMax (durationMax: number) { | ||
511 | this.and.push('"video"."duration" <= :durationMax') | ||
512 | this.replacements.durationMax = durationMax | ||
513 | } | ||
514 | |||
515 | private groupForTrending (trendingDays: number) { | ||
516 | const viewsGteDate = new Date(new Date().getTime() - (24 * 3600 * 1000) * trendingDays) | ||
517 | |||
518 | this.joins.push('LEFT JOIN "videoView" ON "video"."id" = "videoView"."videoId" AND "videoView"."startDate" >= :viewsGteDate') | ||
519 | this.replacements.viewsGteDate = viewsGteDate | ||
520 | |||
521 | this.attributes.push('COALESCE(SUM("videoView"."views"), 0) AS "score"') | ||
522 | |||
523 | this.group = 'GROUP BY "video"."id"' | ||
524 | } | ||
525 | |||
526 | private groupForHotOrBest (trendingAlgorithm: string, user?: MUserAccountId) { | ||
527 | /** | ||
528 | * "Hotness" is a measure based on absolute view/comment/like/dislike numbers, | ||
529 | * with fixed weights only applied to their log values. | ||
530 | * | ||
531 | * This algorithm gives little chance for an old video to have a good score, | ||
532 | * for which recent spikes in interactions could be a sign of "hotness" and | ||
533 | * justify a better score. However there are multiple ways to achieve that | ||
534 | * goal, which is left for later. Yes, this is a TODO :) | ||
535 | * | ||
536 | * notes: | ||
537 | * - weights and base score are in number of half-days. | ||
538 | * - all comments are counted, regardless of being written by the video author or not | ||
539 | * see https://github.com/reddit-archive/reddit/blob/master/r2/r2/lib/db/_sorts.pyx#L47-L58 | ||
540 | * - we have less interactions than on reddit, so multiply weights by an arbitrary factor | ||
541 | */ | ||
542 | const weights = { | ||
543 | like: 3 * 50, | ||
544 | dislike: -3 * 50, | ||
545 | view: Math.floor((1 / 3) * 50), | ||
546 | comment: 2 * 50, // a comment takes more time than a like to do, but can be done multiple times | ||
547 | history: -2 * 50 | ||
548 | } | ||
549 | |||
550 | this.joins.push('LEFT JOIN "videoComment" ON "video"."id" = "videoComment"."videoId"') | ||
551 | |||
552 | let attribute = | ||
553 | `LOG(GREATEST(1, "video"."likes" - 1)) * ${weights.like} ` + // likes (+) | ||
554 | `+ LOG(GREATEST(1, "video"."dislikes" - 1)) * ${weights.dislike} ` + // dislikes (-) | ||
555 | `+ LOG("video"."views" + 1) * ${weights.view} ` + // views (+) | ||
556 | `+ LOG(GREATEST(1, COUNT(DISTINCT "videoComment"."id"))) * ${weights.comment} ` + // comments (+) | ||
557 | '+ (SELECT (EXTRACT(epoch FROM "video"."publishedAt") - 1446156582) / 47000) ' // base score (in number of half-days) | ||
558 | |||
559 | if (trendingAlgorithm === 'best' && user) { | ||
560 | this.joins.push( | ||
561 | 'LEFT JOIN "userVideoHistory" ON "video"."id" = "userVideoHistory"."videoId" AND "userVideoHistory"."userId" = :bestUser' | ||
562 | ) | ||
563 | this.replacements.bestUser = user.id | ||
564 | |||
565 | attribute += `+ POWER(COUNT(DISTINCT "userVideoHistory"."id"), 2.0) * ${weights.history} ` | ||
566 | } | ||
567 | |||
568 | attribute += 'AS "score"' | ||
569 | this.attributes.push(attribute) | ||
570 | |||
571 | this.group = 'GROUP BY "video"."id"' | ||
572 | } | ||
573 | |||
574 | private setSort (sort: string) { | ||
575 | if (sort === '-originallyPublishedAt' || sort === 'originallyPublishedAt') { | ||
576 | this.attributes.push('COALESCE("video"."originallyPublishedAt", "video"."publishedAt") AS "publishedAtForOrder"') | ||
577 | } | ||
578 | |||
579 | this.sort = this.buildOrder(sort) | ||
580 | } | ||
581 | |||
582 | private buildOrder (value: string) { | ||
583 | const { direction, field } = buildDirectionAndField(value) | ||
584 | if (field.match(/^[a-zA-Z."]+$/) === null) throw new Error('Invalid sort column ' + field) | ||
585 | |||
586 | if (field.toLowerCase() === 'random') return 'ORDER BY RANDOM()' | ||
587 | |||
588 | if ([ 'trending', 'hot', 'best' ].includes(field.toLowerCase())) { // Sort by aggregation | ||
589 | return `ORDER BY "score" ${direction}, "video"."views" ${direction}` | ||
590 | } | ||
591 | |||
592 | let firstSort: string | ||
593 | |||
594 | if (field.toLowerCase() === 'match') { // Search | ||
595 | firstSort = '"similarity"' | ||
596 | } else if (field === 'originallyPublishedAt') { | ||
597 | firstSort = '"publishedAtForOrder"' | ||
598 | } else if (field.includes('.')) { | ||
599 | firstSort = field | ||
600 | } else { | ||
601 | firstSort = `"video"."${field}"` | ||
602 | } | ||
603 | |||
604 | return `ORDER BY ${firstSort} ${direction}, "video"."id" ASC` | ||
605 | } | ||
606 | |||
607 | private setLimit (countArg: number) { | ||
608 | const count = parseInt(countArg + '', 10) | ||
609 | this.limit = `LIMIT ${count}` | ||
610 | } | ||
611 | |||
612 | private setOffset (startArg: number) { | ||
613 | const start = parseInt(startArg + '', 10) | ||
614 | this.offset = `OFFSET ${start}` | ||
615 | } | ||
616 | } | ||
diff --git a/server/models/video/sql/videos-model-list-query-builder.ts b/server/models/video/sql/videos-model-list-query-builder.ts new file mode 100644 index 000000000..e61c51de8 --- /dev/null +++ b/server/models/video/sql/videos-model-list-query-builder.ts | |||
@@ -0,0 +1,71 @@ | |||
1 | import { Sequelize } from 'sequelize' | ||
2 | import { AbstractVideosModelQueryBuilder } from './shared/abstract-videos-model-query-builder' | ||
3 | import { VideoModelBuilder } from './shared/video-model-builder' | ||
4 | import { BuildVideosListQueryOptions, VideosIdListQueryBuilder } from './videos-id-list-query-builder' | ||
5 | |||
6 | /** | ||
7 | * | ||
8 | * Build videos list SQL query and create video models | ||
9 | * | ||
10 | */ | ||
11 | |||
12 | export class VideosModelListQueryBuilder extends AbstractVideosModelQueryBuilder { | ||
13 | protected attributes: { [key: string]: string } | ||
14 | |||
15 | private innerQuery: string | ||
16 | private innerSort: string | ||
17 | |||
18 | private readonly videoModelBuilder: VideoModelBuilder | ||
19 | |||
20 | constructor (protected readonly sequelize: Sequelize) { | ||
21 | super('list') | ||
22 | |||
23 | this.videoModelBuilder = new VideoModelBuilder(this.mode, this.tables) | ||
24 | } | ||
25 | |||
26 | queryVideos (options: BuildVideosListQueryOptions) { | ||
27 | this.buildInnerQuery(options) | ||
28 | this.buildListQueryFromIdsQuery(options) | ||
29 | |||
30 | return this.runQuery() | ||
31 | .then(rows => this.videoModelBuilder.buildVideosFromRows(rows)) | ||
32 | } | ||
33 | |||
34 | private buildInnerQuery (options: BuildVideosListQueryOptions) { | ||
35 | const idsQueryBuilder = new VideosIdListQueryBuilder(this.sequelize) | ||
36 | const { query, sort, replacements } = idsQueryBuilder.getIdsListQueryAndSort(options) | ||
37 | |||
38 | this.replacements = replacements | ||
39 | this.innerQuery = query | ||
40 | this.innerSort = sort | ||
41 | } | ||
42 | |||
43 | private buildListQueryFromIdsQuery (options: BuildVideosListQueryOptions) { | ||
44 | this.attributes = { | ||
45 | '"video".*': '' | ||
46 | } | ||
47 | |||
48 | this.addJoin('INNER JOIN "video" ON "tmp"."id" = "video"."id"') | ||
49 | |||
50 | this.includeChannels() | ||
51 | this.includeAccounts() | ||
52 | this.includeThumbnails() | ||
53 | |||
54 | if (options.withFiles) { | ||
55 | this.includeWebtorrentFiles() | ||
56 | this.includeStreamingPlaylistFiles() | ||
57 | } | ||
58 | |||
59 | if (options.user) { | ||
60 | this.includeUserHistory(options.user.id) | ||
61 | } | ||
62 | |||
63 | if (options.videoPlaylistId) { | ||
64 | this.includePlaylist(options.videoPlaylistId) | ||
65 | } | ||
66 | |||
67 | const select = this.buildSelect() | ||
68 | |||
69 | this.query = `${select} FROM (${this.innerQuery}) AS "tmp" ${this.joins} ${this.innerSort}` | ||
70 | } | ||
71 | } | ||
diff --git a/server/models/video/tag.ts b/server/models/video/tag.ts index d04205703..c1eebe27f 100644 --- a/server/models/video/tag.ts +++ b/server/models/video/tag.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { col, fn, QueryTypes, Transaction } from 'sequelize' | 1 | import { col, fn, QueryTypes, Transaction } from 'sequelize' |
2 | import { AllowNull, BelongsToMany, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { AllowNull, BelongsToMany, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { MTag } from '@server/types/models' | 3 | import { MTag } from '@server/types/models' |
4 | import { AttributesOnly } from '@shared/core-utils' | ||
4 | import { VideoPrivacy, VideoState } from '../../../shared/models/videos' | 5 | import { VideoPrivacy, VideoState } from '../../../shared/models/videos' |
5 | import { isVideoTagValid } from '../../helpers/custom-validators/videos' | 6 | import { isVideoTagValid } from '../../helpers/custom-validators/videos' |
6 | import { throwIfNotValid } from '../utils' | 7 | import { throwIfNotValid } from '../utils' |
@@ -21,7 +22,7 @@ import { VideoTagModel } from './video-tag' | |||
21 | } | 22 | } |
22 | ] | 23 | ] |
23 | }) | 24 | }) |
24 | export class TagModel extends Model { | 25 | export class TagModel extends Model<Partial<AttributesOnly<TagModel>>> { |
25 | 26 | ||
26 | @AllowNull(false) | 27 | @AllowNull(false) |
27 | @Is('VideoTag', value => throwIfNotValid(value, isVideoTagValid, 'tag')) | 28 | @Is('VideoTag', value => throwIfNotValid(value, isVideoTagValid, 'tag')) |
diff --git a/server/models/video/thumbnail.ts b/server/models/video/thumbnail.ts index f1187c8d6..3388478d9 100644 --- a/server/models/video/thumbnail.ts +++ b/server/models/video/thumbnail.ts | |||
@@ -17,6 +17,7 @@ import { | |||
17 | } from 'sequelize-typescript' | 17 | } from 'sequelize-typescript' |
18 | import { afterCommitIfTransaction } from '@server/helpers/database-utils' | 18 | import { afterCommitIfTransaction } from '@server/helpers/database-utils' |
19 | import { MThumbnail, MThumbnailVideo, MVideo } from '@server/types/models' | 19 | import { MThumbnail, MThumbnailVideo, MVideo } from '@server/types/models' |
20 | import { AttributesOnly } from '@shared/core-utils' | ||
20 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' | 21 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' |
21 | import { logger } from '../../helpers/logger' | 22 | import { logger } from '../../helpers/logger' |
22 | import { CONFIG } from '../../initializers/config' | 23 | import { CONFIG } from '../../initializers/config' |
@@ -40,7 +41,7 @@ import { VideoPlaylistModel } from './video-playlist' | |||
40 | } | 41 | } |
41 | ] | 42 | ] |
42 | }) | 43 | }) |
43 | export class ThumbnailModel extends Model { | 44 | export class ThumbnailModel extends Model<Partial<AttributesOnly<ThumbnailModel>>> { |
44 | 45 | ||
45 | @AllowNull(false) | 46 | @AllowNull(false) |
46 | @Column | 47 | @Column |
diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts index aa18896da..98f4ec9c5 100644 --- a/server/models/video/video-blacklist.ts +++ b/server/models/video/video-blacklist.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { FindOptions } from 'sequelize' | 1 | import { FindOptions } from 'sequelize' |
2 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { MVideoBlacklist, MVideoBlacklistFormattable } from '@server/types/models' | 3 | import { MVideoBlacklist, MVideoBlacklistFormattable } from '@server/types/models' |
4 | import { AttributesOnly } from '@shared/core-utils' | ||
4 | import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos' | 5 | import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos' |
5 | import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist' | 6 | import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist' |
6 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | 7 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' |
@@ -18,7 +19,7 @@ import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel | |||
18 | } | 19 | } |
19 | ] | 20 | ] |
20 | }) | 21 | }) |
21 | export class VideoBlacklistModel extends Model { | 22 | export class VideoBlacklistModel extends Model<Partial<AttributesOnly<VideoBlacklistModel>>> { |
22 | 23 | ||
23 | @AllowNull(true) | 24 | @AllowNull(true) |
24 | @Is('VideoBlacklistReason', value => throwIfNotValid(value, isVideoBlacklistReasonValid, 'reason', true)) | 25 | @Is('VideoBlacklistReason', value => throwIfNotValid(value, isVideoBlacklistReasonValid, 'reason', true)) |
diff --git a/server/models/video/video-caption.ts b/server/models/video/video-caption.ts index bfdec73e9..d24be56c3 100644 --- a/server/models/video/video-caption.ts +++ b/server/models/video/video-caption.ts | |||
@@ -15,8 +15,9 @@ import { | |||
15 | Table, | 15 | Table, |
16 | UpdatedAt | 16 | UpdatedAt |
17 | } from 'sequelize-typescript' | 17 | } from 'sequelize-typescript' |
18 | import { v4 as uuidv4 } from 'uuid' | 18 | import { buildUUID } from '@server/helpers/uuid' |
19 | import { MVideo, MVideoCaption, MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/types/models' | 19 | import { MVideo, MVideoCaption, MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/types/models' |
20 | import { AttributesOnly } from '@shared/core-utils' | ||
20 | import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model' | 21 | import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model' |
21 | import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions' | 22 | import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions' |
22 | import { logger } from '../../helpers/logger' | 23 | import { logger } from '../../helpers/logger' |
@@ -57,7 +58,7 @@ export enum ScopeNames { | |||
57 | } | 58 | } |
58 | ] | 59 | ] |
59 | }) | 60 | }) |
60 | export class VideoCaptionModel extends Model { | 61 | export class VideoCaptionModel extends Model<Partial<AttributesOnly<VideoCaptionModel>>> { |
61 | @CreatedAt | 62 | @CreatedAt |
62 | createdAt: Date | 63 | createdAt: Date |
63 | 64 | ||
@@ -90,9 +91,9 @@ export class VideoCaptionModel extends Model { | |||
90 | Video: VideoModel | 91 | Video: VideoModel |
91 | 92 | ||
92 | @BeforeDestroy | 93 | @BeforeDestroy |
93 | static async removeFiles (instance: VideoCaptionModel) { | 94 | static async removeFiles (instance: VideoCaptionModel, options) { |
94 | if (!instance.Video) { | 95 | if (!instance.Video) { |
95 | instance.Video = await instance.$get('Video') | 96 | instance.Video = await instance.$get('Video', { transaction: options.transaction }) |
96 | } | 97 | } |
97 | 98 | ||
98 | if (instance.isOwned()) { | 99 | if (instance.isOwned()) { |
@@ -108,7 +109,7 @@ export class VideoCaptionModel extends Model { | |||
108 | return undefined | 109 | return undefined |
109 | } | 110 | } |
110 | 111 | ||
111 | static loadByVideoIdAndLanguage (videoId: string | number, language: string): Promise<MVideoCaptionVideo> { | 112 | static loadByVideoIdAndLanguage (videoId: string | number, language: string, transaction?: Transaction): Promise<MVideoCaptionVideo> { |
112 | const videoInclude = { | 113 | const videoInclude = { |
113 | model: VideoModel.unscoped(), | 114 | model: VideoModel.unscoped(), |
114 | attributes: [ 'id', 'remote', 'uuid' ], | 115 | attributes: [ 'id', 'remote', 'uuid' ], |
@@ -121,7 +122,8 @@ export class VideoCaptionModel extends Model { | |||
121 | }, | 122 | }, |
122 | include: [ | 123 | include: [ |
123 | videoInclude | 124 | videoInclude |
124 | ] | 125 | ], |
126 | transaction | ||
125 | } | 127 | } |
126 | 128 | ||
127 | return VideoCaptionModel.findOne(query) | 129 | return VideoCaptionModel.findOne(query) |
@@ -144,19 +146,21 @@ export class VideoCaptionModel extends Model { | |||
144 | } | 146 | } |
145 | 147 | ||
146 | static async insertOrReplaceLanguage (caption: MVideoCaption, transaction: Transaction) { | 148 | static async insertOrReplaceLanguage (caption: MVideoCaption, transaction: Transaction) { |
147 | const existing = await VideoCaptionModel.loadByVideoIdAndLanguage(caption.videoId, caption.language) | 149 | const existing = await VideoCaptionModel.loadByVideoIdAndLanguage(caption.videoId, caption.language, transaction) |
150 | |||
148 | // Delete existing file | 151 | // Delete existing file |
149 | if (existing) await existing.destroy({ transaction }) | 152 | if (existing) await existing.destroy({ transaction }) |
150 | 153 | ||
151 | return caption.save({ transaction }) | 154 | return caption.save({ transaction }) |
152 | } | 155 | } |
153 | 156 | ||
154 | static listVideoCaptions (videoId: number): Promise<MVideoCaptionVideo[]> { | 157 | static listVideoCaptions (videoId: number, transaction?: Transaction): Promise<MVideoCaptionVideo[]> { |
155 | const query = { | 158 | const query = { |
156 | order: [ [ 'language', 'ASC' ] ] as OrderItem[], | 159 | order: [ [ 'language', 'ASC' ] ] as OrderItem[], |
157 | where: { | 160 | where: { |
158 | videoId | 161 | videoId |
159 | } | 162 | }, |
163 | transaction | ||
160 | } | 164 | } |
161 | 165 | ||
162 | return VideoCaptionModel.scope(ScopeNames.WITH_VIDEO_UUID_AND_REMOTE).findAll(query) | 166 | return VideoCaptionModel.scope(ScopeNames.WITH_VIDEO_UUID_AND_REMOTE).findAll(query) |
@@ -178,7 +182,7 @@ export class VideoCaptionModel extends Model { | |||
178 | } | 182 | } |
179 | 183 | ||
180 | static generateCaptionName (language: string) { | 184 | static generateCaptionName (language: string) { |
181 | return `${uuidv4()}-${language}.vtt` | 185 | return `${buildUUID()}-${language}.vtt` |
182 | } | 186 | } |
183 | 187 | ||
184 | isOwned () { | 188 | isOwned () { |
@@ -210,4 +214,10 @@ export class VideoCaptionModel extends Model { | |||
210 | 214 | ||
211 | return this.fileUrl | 215 | return this.fileUrl |
212 | } | 216 | } |
217 | |||
218 | isEqual (this: MVideoCaption, other: MVideoCaption) { | ||
219 | if (this.fileUrl) return this.fileUrl === other.fileUrl | ||
220 | |||
221 | return this.filename === other.filename | ||
222 | } | ||
213 | } | 223 | } |
diff --git a/server/models/video/video-change-ownership.ts b/server/models/video/video-change-ownership.ts index 298e8bfe2..7d20a954d 100644 --- a/server/models/video/video-change-ownership.ts +++ b/server/models/video/video-change-ownership.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { MVideoChangeOwnershipFormattable, MVideoChangeOwnershipFull } from '@server/types/models/video/video-change-ownership' | 2 | import { MVideoChangeOwnershipFormattable, MVideoChangeOwnershipFull } from '@server/types/models/video/video-change-ownership' |
3 | import { AttributesOnly } from '@shared/core-utils' | ||
3 | import { VideoChangeOwnership, VideoChangeOwnershipStatus } from '../../../shared/models/videos' | 4 | import { VideoChangeOwnership, VideoChangeOwnershipStatus } from '../../../shared/models/videos' |
4 | import { AccountModel } from '../account/account' | 5 | import { AccountModel } from '../account/account' |
5 | import { getSort } from '../utils' | 6 | import { getSort } from '../utils' |
@@ -53,7 +54,7 @@ enum ScopeNames { | |||
53 | ] | 54 | ] |
54 | } | 55 | } |
55 | })) | 56 | })) |
56 | export class VideoChangeOwnershipModel extends Model { | 57 | export class VideoChangeOwnershipModel extends Model<Partial<AttributesOnly<VideoChangeOwnershipModel>>> { |
57 | @CreatedAt | 58 | @CreatedAt |
58 | createdAt: Date | 59 | createdAt: Date |
59 | 60 | ||
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 081b21f2d..183e7448c 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -19,6 +19,7 @@ import { | |||
19 | } from 'sequelize-typescript' | 19 | } from 'sequelize-typescript' |
20 | import { setAsUpdated } from '@server/helpers/database-utils' | 20 | import { setAsUpdated } from '@server/helpers/database-utils' |
21 | import { MAccountActor } from '@server/types/models' | 21 | import { MAccountActor } from '@server/types/models' |
22 | import { AttributesOnly } from '@shared/core-utils' | ||
22 | import { ActivityPubActor } from '../../../shared/models/activitypub' | 23 | import { ActivityPubActor } from '../../../shared/models/activitypub' |
23 | import { VideoChannel, VideoChannelSummary } from '../../../shared/models/videos' | 24 | import { VideoChannel, VideoChannelSummary } from '../../../shared/models/videos' |
24 | import { | 25 | import { |
@@ -36,9 +37,9 @@ import { | |||
36 | MChannelSummaryFormattable | 37 | MChannelSummaryFormattable |
37 | } from '../../types/models/video' | 38 | } from '../../types/models/video' |
38 | import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' | 39 | import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' |
39 | import { ActorImageModel } from '../account/actor-image' | 40 | import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor' |
40 | import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' | 41 | import { ActorFollowModel } from '../actor/actor-follow' |
41 | import { ActorFollowModel } from '../activitypub/actor-follow' | 42 | import { ActorImageModel } from '../actor/actor-image' |
42 | import { ServerModel } from '../server/server' | 43 | import { ServerModel } from '../server/server' |
43 | import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' | 44 | import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' |
44 | import { VideoModel } from './video' | 45 | import { VideoModel } from './video' |
@@ -246,7 +247,7 @@ export type SummaryOptions = { | |||
246 | } | 247 | } |
247 | ] | 248 | ] |
248 | }) | 249 | }) |
249 | export class VideoChannelModel extends Model { | 250 | export class VideoChannelModel extends Model<Partial<AttributesOnly<VideoChannelModel>>> { |
250 | 251 | ||
251 | @AllowNull(false) | 252 | @AllowNull(false) |
252 | @Is('VideoChannelName', value => throwIfNotValid(value, isVideoChannelNameValid, 'name')) | 253 | @Is('VideoChannelName', value => throwIfNotValid(value, isVideoChannelNameValid, 'name')) |
@@ -290,8 +291,7 @@ export class VideoChannelModel extends Model { | |||
290 | @BelongsTo(() => AccountModel, { | 291 | @BelongsTo(() => AccountModel, { |
291 | foreignKey: { | 292 | foreignKey: { |
292 | allowNull: false | 293 | allowNull: false |
293 | }, | 294 | } |
294 | hooks: true | ||
295 | }) | 295 | }) |
296 | Account: AccountModel | 296 | Account: AccountModel |
297 | 297 | ||
@@ -433,8 +433,8 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
433 | sort: string | 433 | sort: string |
434 | }) { | 434 | }) { |
435 | const attributesInclude = [] | 435 | const attributesInclude = [] |
436 | const escapedSearch = VideoModel.sequelize.escape(options.search) | 436 | const escapedSearch = VideoChannelModel.sequelize.escape(options.search) |
437 | const escapedLikeSearch = VideoModel.sequelize.escape('%' + options.search + '%') | 437 | const escapedLikeSearch = VideoChannelModel.sequelize.escape('%' + options.search + '%') |
438 | attributesInclude.push(createSimilarityAttribute('VideoChannelModel.name', options.search)) | 438 | attributesInclude.push(createSimilarityAttribute('VideoChannelModel.name', options.search)) |
439 | 439 | ||
440 | const query = { | 440 | const query = { |
@@ -521,10 +521,10 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
521 | }) | 521 | }) |
522 | } | 522 | } |
523 | 523 | ||
524 | static loadAndPopulateAccount (id: number): Promise<MChannelBannerAccountDefault> { | 524 | static loadAndPopulateAccount (id: number, transaction?: Transaction): Promise<MChannelBannerAccountDefault> { |
525 | return VideoChannelModel.unscoped() | 525 | return VideoChannelModel.unscoped() |
526 | .scope([ ScopeNames.WITH_ACTOR_BANNER, ScopeNames.WITH_ACCOUNT ]) | 526 | .scope([ ScopeNames.WITH_ACTOR_BANNER, ScopeNames.WITH_ACCOUNT ]) |
527 | .findByPk(id) | 527 | .findByPk(id, { transaction }) |
528 | } | 528 | } |
529 | 529 | ||
530 | static loadByUrlAndPopulateAccount (url: string): Promise<MChannelBannerAccountDefault> { | 530 | static loadByUrlAndPopulateAccount (url: string): Promise<MChannelBannerAccountDefault> { |
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index 151c2bc81..e933989ae 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts | |||
@@ -16,10 +16,11 @@ import { | |||
16 | } from 'sequelize-typescript' | 16 | } from 'sequelize-typescript' |
17 | import { getServerActor } from '@server/models/application/application' | 17 | import { getServerActor } from '@server/models/application/application' |
18 | import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' | 18 | import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' |
19 | import { AttributesOnly } from '@shared/core-utils' | ||
19 | import { VideoPrivacy } from '@shared/models' | 20 | import { VideoPrivacy } from '@shared/models' |
20 | import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects' | 21 | import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects' |
21 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' | 22 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' |
22 | import { VideoComment, VideoCommentAdmin } from '../../../shared/models/videos/video-comment.model' | 23 | import { VideoComment, VideoCommentAdmin } from '../../../shared/models/videos/comment/video-comment.model' |
23 | import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor' | 24 | import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor' |
24 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 25 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
25 | import { regexpCapture } from '../../helpers/regexp' | 26 | import { regexpCapture } from '../../helpers/regexp' |
@@ -39,7 +40,7 @@ import { | |||
39 | } from '../../types/models/video' | 40 | } from '../../types/models/video' |
40 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' | 41 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' |
41 | import { AccountModel } from '../account/account' | 42 | import { AccountModel } from '../account/account' |
42 | import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' | 43 | import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor' |
43 | import { | 44 | import { |
44 | buildBlockedAccountSQL, | 45 | buildBlockedAccountSQL, |
45 | buildBlockedAccountSQLOptimized, | 46 | buildBlockedAccountSQLOptimized, |
@@ -68,14 +69,10 @@ export enum ScopeNames { | |||
68 | Sequelize.literal( | 69 | Sequelize.literal( |
69 | '(' + | 70 | '(' + |
70 | 'WITH "blocklist" AS (' + buildBlockedAccountSQL(blockerAccountIds) + ')' + | 71 | 'WITH "blocklist" AS (' + buildBlockedAccountSQL(blockerAccountIds) + ')' + |
71 | 'SELECT COUNT("replies"."id") - (' + | 72 | 'SELECT COUNT("replies"."id") ' + |
72 | 'SELECT COUNT("replies"."id") ' + | ||
73 | 'FROM "videoComment" AS "replies" ' + | ||
74 | 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' + | ||
75 | 'AND "accountId" IN (SELECT "id" FROM "blocklist")' + | ||
76 | ')' + | ||
77 | 'FROM "videoComment" AS "replies" ' + | 73 | 'FROM "videoComment" AS "replies" ' + |
78 | 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' + | 74 | 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' + |
75 | 'AND "deletedAt" IS NULL ' + | ||
79 | 'AND "accountId" NOT IN (SELECT "id" FROM "blocklist")' + | 76 | 'AND "accountId" NOT IN (SELECT "id" FROM "blocklist")' + |
80 | ')' | 77 | ')' |
81 | ), | 78 | ), |
@@ -173,7 +170,7 @@ export enum ScopeNames { | |||
173 | } | 170 | } |
174 | ] | 171 | ] |
175 | }) | 172 | }) |
176 | export class VideoCommentModel extends Model { | 173 | export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoCommentModel>>> { |
177 | @CreatedAt | 174 | @CreatedAt |
178 | createdAt: Date | 175 | createdAt: Date |
179 | 176 | ||
@@ -742,6 +739,12 @@ export class VideoCommentModel extends Model { | |||
742 | return this.Account.isOwned() | 739 | return this.Account.isOwned() |
743 | } | 740 | } |
744 | 741 | ||
742 | markAsDeleted () { | ||
743 | this.text = '' | ||
744 | this.deletedAt = new Date() | ||
745 | this.accountId = null | ||
746 | } | ||
747 | |||
745 | isDeleted () { | 748 | isDeleted () { |
746 | return this.deletedAt !== null | 749 | return this.deletedAt !== null |
747 | } | 750 | } |
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index 0b5946149..22cf63804 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts | |||
@@ -25,6 +25,7 @@ import { logger } from '@server/helpers/logger' | |||
25 | import { extractVideo } from '@server/helpers/video' | 25 | import { extractVideo } from '@server/helpers/video' |
26 | import { getTorrentFilePath } from '@server/lib/video-paths' | 26 | import { getTorrentFilePath } from '@server/lib/video-paths' |
27 | import { MStreamingPlaylistVideo, MVideo, MVideoWithHost } from '@server/types/models' | 27 | import { MStreamingPlaylistVideo, MVideo, MVideoWithHost } from '@server/types/models' |
28 | import { AttributesOnly } from '@shared/core-utils' | ||
28 | import { | 29 | import { |
29 | isVideoFileExtnameValid, | 30 | isVideoFileExtnameValid, |
30 | isVideoFileInfoHashValid, | 31 | isVideoFileInfoHashValid, |
@@ -149,7 +150,7 @@ export enum ScopeNames { | |||
149 | } | 150 | } |
150 | ] | 151 | ] |
151 | }) | 152 | }) |
152 | export class VideoFileModel extends Model { | 153 | export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel>>> { |
153 | @CreatedAt | 154 | @CreatedAt |
154 | createdAt: Date | 155 | createdAt: Date |
155 | 156 | ||
diff --git a/server/models/video/video-import.ts b/server/models/video/video-import.ts index 8324166cc..5c73fb07c 100644 --- a/server/models/video/video-import.ts +++ b/server/models/video/video-import.ts | |||
@@ -13,15 +13,16 @@ import { | |||
13 | Table, | 13 | Table, |
14 | UpdatedAt | 14 | UpdatedAt |
15 | } from 'sequelize-typescript' | 15 | } from 'sequelize-typescript' |
16 | import { afterCommitIfTransaction } from '@server/helpers/database-utils' | ||
16 | import { MVideoImportDefault, MVideoImportFormattable } from '@server/types/models/video/video-import' | 17 | import { MVideoImportDefault, MVideoImportFormattable } from '@server/types/models/video/video-import' |
18 | import { AttributesOnly } from '@shared/core-utils' | ||
17 | import { VideoImport, VideoImportState } from '../../../shared' | 19 | import { VideoImport, VideoImportState } from '../../../shared' |
18 | import { isVideoImportStateValid, isVideoImportTargetUrlValid } from '../../helpers/custom-validators/video-imports' | 20 | import { isVideoImportStateValid, isVideoImportTargetUrlValid } from '../../helpers/custom-validators/video-imports' |
19 | import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos' | 21 | import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos' |
20 | import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers/constants' | 22 | import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers/constants' |
21 | import { UserModel } from '../account/user' | 23 | import { UserModel } from '../user/user' |
22 | import { getSort, throwIfNotValid } from '../utils' | 24 | import { getSort, throwIfNotValid } from '../utils' |
23 | import { ScopeNames as VideoModelScopeNames, VideoModel } from './video' | 25 | import { ScopeNames as VideoModelScopeNames, VideoModel } from './video' |
24 | import { afterCommitIfTransaction } from '@server/helpers/database-utils' | ||
25 | 26 | ||
26 | @DefaultScope(() => ({ | 27 | @DefaultScope(() => ({ |
27 | include: [ | 28 | include: [ |
@@ -52,7 +53,7 @@ import { afterCommitIfTransaction } from '@server/helpers/database-utils' | |||
52 | } | 53 | } |
53 | ] | 54 | ] |
54 | }) | 55 | }) |
55 | export class VideoImportModel extends Model { | 56 | export class VideoImportModel extends Model<Partial<AttributesOnly<VideoImportModel>>> { |
56 | @CreatedAt | 57 | @CreatedAt |
57 | createdAt: Date | 58 | createdAt: Date |
58 | 59 | ||
diff --git a/server/models/video/video-live.ts b/server/models/video/video-live.ts index cb4a9b896..014491d50 100644 --- a/server/models/video/video-live.ts +++ b/server/models/video/video-live.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, DefaultScope, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, DefaultScope, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { WEBSERVER } from '@server/initializers/constants' | 2 | import { WEBSERVER } from '@server/initializers/constants' |
3 | import { MVideoLive, MVideoLiveVideo } from '@server/types/models' | 3 | import { MVideoLive, MVideoLiveVideo } from '@server/types/models' |
4 | import { AttributesOnly } from '@shared/core-utils' | ||
4 | import { LiveVideo, VideoState } from '@shared/models' | 5 | import { LiveVideo, VideoState } from '@shared/models' |
5 | import { VideoModel } from './video' | 6 | import { VideoModel } from './video' |
6 | import { VideoBlacklistModel } from './video-blacklist' | 7 | import { VideoBlacklistModel } from './video-blacklist' |
@@ -28,7 +29,7 @@ import { VideoBlacklistModel } from './video-blacklist' | |||
28 | } | 29 | } |
29 | ] | 30 | ] |
30 | }) | 31 | }) |
31 | export class VideoLiveModel extends Model { | 32 | export class VideoLiveModel extends Model<Partial<AttributesOnly<VideoLiveModel>>> { |
32 | 33 | ||
33 | @AllowNull(true) | 34 | @AllowNull(true) |
34 | @Column(DataType.STRING) | 35 | @Column(DataType.STRING) |
diff --git a/server/models/video/video-playlist-element.ts b/server/models/video/video-playlist-element.ts index d2d7e2740..e6906cb19 100644 --- a/server/models/video/video-playlist-element.ts +++ b/server/models/video/video-playlist-element.ts | |||
@@ -32,6 +32,7 @@ import { AccountModel } from '../account/account' | |||
32 | import { getSort, throwIfNotValid } from '../utils' | 32 | import { getSort, throwIfNotValid } from '../utils' |
33 | import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video' | 33 | import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video' |
34 | import { VideoPlaylistModel } from './video-playlist' | 34 | import { VideoPlaylistModel } from './video-playlist' |
35 | import { AttributesOnly } from '@shared/core-utils' | ||
35 | 36 | ||
36 | @Table({ | 37 | @Table({ |
37 | tableName: 'videoPlaylistElement', | 38 | tableName: 'videoPlaylistElement', |
@@ -48,7 +49,7 @@ import { VideoPlaylistModel } from './video-playlist' | |||
48 | } | 49 | } |
49 | ] | 50 | ] |
50 | }) | 51 | }) |
51 | export class VideoPlaylistElementModel extends Model { | 52 | export class VideoPlaylistElementModel extends Model<Partial<AttributesOnly<VideoPlaylistElementModel>>> { |
52 | @CreatedAt | 53 | @CreatedAt |
53 | createdAt: Date | 54 | createdAt: Date |
54 | 55 | ||
@@ -274,7 +275,8 @@ export class VideoPlaylistElementModel extends Model { | |||
274 | validate: false // We use a literal to update the position | 275 | validate: false // We use a literal to update the position |
275 | } | 276 | } |
276 | 277 | ||
277 | return VideoPlaylistElementModel.update({ position: Sequelize.literal(`${newPosition} + "position" - ${firstPosition}`) }, query) | 278 | const positionQuery = Sequelize.literal(`${newPosition} + "position" - ${firstPosition}`) |
279 | return VideoPlaylistElementModel.update({ position: positionQuery as any }, query) | ||
278 | } | 280 | } |
279 | 281 | ||
280 | static increasePositionOf ( | 282 | static increasePositionOf ( |
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index efe5be36d..af81c9906 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { join } from 'path' | 1 | import { join } from 'path' |
2 | import { FindOptions, literal, Op, ScopeOptions, Transaction, WhereOptions } from 'sequelize' | 2 | import { FindOptions, literal, Op, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' |
3 | import { | 3 | import { |
4 | AllowNull, | 4 | AllowNull, |
5 | BelongsTo, | 5 | BelongsTo, |
@@ -17,8 +17,10 @@ import { | |||
17 | Table, | 17 | Table, |
18 | UpdatedAt | 18 | UpdatedAt |
19 | } from 'sequelize-typescript' | 19 | } from 'sequelize-typescript' |
20 | import { v4 as uuidv4 } from 'uuid' | 20 | import { setAsUpdated } from '@server/helpers/database-utils' |
21 | import { buildUUID, uuidToShort } from '@server/helpers/uuid' | ||
21 | import { MAccountId, MChannelId } from '@server/types/models' | 22 | import { MAccountId, MChannelId } from '@server/types/models' |
23 | import { AttributesOnly } from '@shared/core-utils' | ||
22 | import { ActivityIconObject } from '../../../shared/models/activitypub/objects' | 24 | import { ActivityIconObject } from '../../../shared/models/activitypub/objects' |
23 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' | 25 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' |
24 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' | 26 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' |
@@ -50,11 +52,19 @@ import { | |||
50 | MVideoPlaylistIdWithElements | 52 | MVideoPlaylistIdWithElements |
51 | } from '../../types/models/video/video-playlist' | 53 | } from '../../types/models/video/video-playlist' |
52 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' | 54 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' |
53 | import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getPlaylistSort, isOutdated, throwIfNotValid } from '../utils' | 55 | import { ActorModel } from '../actor/actor' |
56 | import { | ||
57 | buildServerIdsFollowedBy, | ||
58 | buildTrigramSearchIndex, | ||
59 | buildWhereIdOrUUID, | ||
60 | createSimilarityAttribute, | ||
61 | getPlaylistSort, | ||
62 | isOutdated, | ||
63 | throwIfNotValid | ||
64 | } from '../utils' | ||
54 | import { ThumbnailModel } from './thumbnail' | 65 | import { ThumbnailModel } from './thumbnail' |
55 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' | 66 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' |
56 | import { VideoPlaylistElementModel } from './video-playlist-element' | 67 | import { VideoPlaylistElementModel } from './video-playlist-element' |
57 | import { ActorModel } from '../activitypub/actor' | ||
58 | 68 | ||
59 | enum ScopeNames { | 69 | enum ScopeNames { |
60 | AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', | 70 | AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', |
@@ -72,6 +82,11 @@ type AvailableForListOptions = { | |||
72 | videoChannelId?: number | 82 | videoChannelId?: number |
73 | listMyPlaylists?: boolean | 83 | listMyPlaylists?: boolean |
74 | search?: string | 84 | search?: string |
85 | withVideos?: boolean | ||
86 | } | ||
87 | |||
88 | function getVideoLengthSelect () { | ||
89 | return 'SELECT COUNT("id") FROM "videoPlaylistElement" WHERE "videoPlaylistId" = "VideoPlaylistModel"."id"' | ||
75 | } | 90 | } |
76 | 91 | ||
77 | @Scopes(() => ({ | 92 | @Scopes(() => ({ |
@@ -87,7 +102,7 @@ type AvailableForListOptions = { | |||
87 | attributes: { | 102 | attributes: { |
88 | include: [ | 103 | include: [ |
89 | [ | 104 | [ |
90 | literal('(SELECT COUNT("id") FROM "videoPlaylistElement" WHERE "videoPlaylistId" = "VideoPlaylistModel"."id")'), | 105 | literal(`(${getVideoLengthSelect()})`), |
91 | 'videosLength' | 106 | 'videosLength' |
92 | ] | 107 | ] |
93 | ] | 108 | ] |
@@ -176,11 +191,28 @@ type AvailableForListOptions = { | |||
176 | }) | 191 | }) |
177 | } | 192 | } |
178 | 193 | ||
194 | if (options.withVideos === true) { | ||
195 | whereAnd.push( | ||
196 | literal(`(${getVideoLengthSelect()}) != 0`) | ||
197 | ) | ||
198 | } | ||
199 | |||
200 | const attributesInclude = [] | ||
201 | |||
179 | if (options.search) { | 202 | if (options.search) { |
203 | const escapedSearch = VideoPlaylistModel.sequelize.escape(options.search) | ||
204 | const escapedLikeSearch = VideoPlaylistModel.sequelize.escape('%' + options.search + '%') | ||
205 | attributesInclude.push(createSimilarityAttribute('VideoPlaylistModel.name', options.search)) | ||
206 | |||
180 | whereAnd.push({ | 207 | whereAnd.push({ |
181 | name: { | 208 | [Op.or]: [ |
182 | [Op.iLike]: '%' + options.search + '%' | 209 | Sequelize.literal( |
183 | } | 210 | 'lower(immutable_unaccent("VideoPlaylistModel"."name")) % lower(immutable_unaccent(' + escapedSearch + '))' |
211 | ), | ||
212 | Sequelize.literal( | ||
213 | 'lower(immutable_unaccent("VideoPlaylistModel"."name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))' | ||
214 | ) | ||
215 | ] | ||
184 | }) | 216 | }) |
185 | } | 217 | } |
186 | 218 | ||
@@ -189,6 +221,9 @@ type AvailableForListOptions = { | |||
189 | } | 221 | } |
190 | 222 | ||
191 | return { | 223 | return { |
224 | attributes: { | ||
225 | include: attributesInclude | ||
226 | }, | ||
192 | where, | 227 | where, |
193 | include: [ | 228 | include: [ |
194 | { | 229 | { |
@@ -209,6 +244,8 @@ type AvailableForListOptions = { | |||
209 | @Table({ | 244 | @Table({ |
210 | tableName: 'videoPlaylist', | 245 | tableName: 'videoPlaylist', |
211 | indexes: [ | 246 | indexes: [ |
247 | buildTrigramSearchIndex('video_playlist_name_trigram', 'name'), | ||
248 | |||
212 | { | 249 | { |
213 | fields: [ 'ownerAccountId' ] | 250 | fields: [ 'ownerAccountId' ] |
214 | }, | 251 | }, |
@@ -221,7 +258,7 @@ type AvailableForListOptions = { | |||
221 | } | 258 | } |
222 | ] | 259 | ] |
223 | }) | 260 | }) |
224 | export class VideoPlaylistModel extends Model { | 261 | export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlaylistModel>>> { |
225 | @CreatedAt | 262 | @CreatedAt |
226 | createdAt: Date | 263 | createdAt: Date |
227 | 264 | ||
@@ -312,6 +349,7 @@ export class VideoPlaylistModel extends Model { | |||
312 | videoChannelId?: number | 349 | videoChannelId?: number |
313 | listMyPlaylists?: boolean | 350 | listMyPlaylists?: boolean |
314 | search?: string | 351 | search?: string |
352 | withVideos?: boolean // false by default | ||
315 | }) { | 353 | }) { |
316 | const query = { | 354 | const query = { |
317 | offset: options.start, | 355 | offset: options.start, |
@@ -329,7 +367,8 @@ export class VideoPlaylistModel extends Model { | |||
329 | accountId: options.accountId, | 367 | accountId: options.accountId, |
330 | videoChannelId: options.videoChannelId, | 368 | videoChannelId: options.videoChannelId, |
331 | listMyPlaylists: options.listMyPlaylists, | 369 | listMyPlaylists: options.listMyPlaylists, |
332 | search: options.search | 370 | search: options.search, |
371 | withVideos: options.withVideos || false | ||
333 | } as AvailableForListOptions | 372 | } as AvailableForListOptions |
334 | ] | 373 | ] |
335 | }, | 374 | }, |
@@ -345,6 +384,21 @@ export class VideoPlaylistModel extends Model { | |||
345 | }) | 384 | }) |
346 | } | 385 | } |
347 | 386 | ||
387 | static searchForApi (options: { | ||
388 | followerActorId: number | ||
389 | start: number | ||
390 | count: number | ||
391 | sort: string | ||
392 | search?: string | ||
393 | }) { | ||
394 | return VideoPlaylistModel.listForApi({ | ||
395 | ...options, | ||
396 | type: VideoPlaylistType.REGULAR, | ||
397 | listMyPlaylists: false, | ||
398 | withVideos: true | ||
399 | }) | ||
400 | } | ||
401 | |||
348 | static listPublicUrlsOfForAP (options: { account?: MAccountId, channel?: MChannelId }, start: number, count: number) { | 402 | static listPublicUrlsOfForAP (options: { account?: MAccountId, channel?: MChannelId }, start: number, count: number) { |
349 | const where = { | 403 | const where = { |
350 | privacy: VideoPlaylistPrivacy.PUBLIC | 404 | privacy: VideoPlaylistPrivacy.PUBLIC |
@@ -443,6 +497,18 @@ export class VideoPlaylistModel extends Model { | |||
443 | return VideoPlaylistModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_THUMBNAIL ]).findOne(query) | 497 | return VideoPlaylistModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_THUMBNAIL ]).findOne(query) |
444 | } | 498 | } |
445 | 499 | ||
500 | static loadByUrlWithAccountAndChannelSummary (url: string): Promise<MVideoPlaylistFullSummary> { | ||
501 | const query = { | ||
502 | where: { | ||
503 | url | ||
504 | } | ||
505 | } | ||
506 | |||
507 | return VideoPlaylistModel | ||
508 | .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY, ScopeNames.WITH_VIDEOS_LENGTH, ScopeNames.WITH_THUMBNAIL ]) | ||
509 | .findOne(query) | ||
510 | } | ||
511 | |||
446 | static getPrivacyLabel (privacy: VideoPlaylistPrivacy) { | 512 | static getPrivacyLabel (privacy: VideoPlaylistPrivacy) { |
447 | return VIDEO_PLAYLIST_PRIVACIES[privacy] || 'Unknown' | 513 | return VIDEO_PLAYLIST_PRIVACIES[privacy] || 'Unknown' |
448 | } | 514 | } |
@@ -479,7 +545,7 @@ export class VideoPlaylistModel extends Model { | |||
479 | generateThumbnailName () { | 545 | generateThumbnailName () { |
480 | const extension = '.jpg' | 546 | const extension = '.jpg' |
481 | 547 | ||
482 | return 'playlist-' + uuidv4() + extension | 548 | return 'playlist-' + buildUUID() + extension |
483 | } | 549 | } |
484 | 550 | ||
485 | getThumbnailUrl () { | 551 | getThumbnailUrl () { |
@@ -495,7 +561,7 @@ export class VideoPlaylistModel extends Model { | |||
495 | } | 561 | } |
496 | 562 | ||
497 | getWatchUrl () { | 563 | getWatchUrl () { |
498 | return WEBSERVER.URL + '/videos/watch/playlist/' + this.uuid | 564 | return WEBSERVER.URL + '/w/p/' + this.uuid |
499 | } | 565 | } |
500 | 566 | ||
501 | getEmbedStaticPath () { | 567 | getEmbedStaticPath () { |
@@ -530,9 +596,11 @@ export class VideoPlaylistModel extends Model { | |||
530 | } | 596 | } |
531 | 597 | ||
532 | setAsRefreshed () { | 598 | setAsRefreshed () { |
533 | this.changed('updatedAt', true) | 599 | return setAsUpdated('videoPlaylist', this.id) |
600 | } | ||
534 | 601 | ||
535 | return this.save() | 602 | setVideosLength (videosLength: number) { |
603 | this.set('videosLength' as any, videosLength, { raw: true }) | ||
536 | } | 604 | } |
537 | 605 | ||
538 | isOwned () { | 606 | isOwned () { |
@@ -549,8 +617,12 @@ export class VideoPlaylistModel extends Model { | |||
549 | return { | 617 | return { |
550 | id: this.id, | 618 | id: this.id, |
551 | uuid: this.uuid, | 619 | uuid: this.uuid, |
620 | shortUUID: uuidToShort(this.uuid), | ||
621 | |||
552 | isLocal: this.isOwned(), | 622 | isLocal: this.isOwned(), |
553 | 623 | ||
624 | url: this.url, | ||
625 | |||
554 | displayName: this.name, | 626 | displayName: this.name, |
555 | description: this.description, | 627 | description: this.description, |
556 | privacy: { | 628 | privacy: { |
diff --git a/server/models/video/video-query-builder.ts b/server/models/video/video-query-builder.ts deleted file mode 100644 index 155afe64b..000000000 --- a/server/models/video/video-query-builder.ts +++ /dev/null | |||
@@ -1,599 +0,0 @@ | |||
1 | import { VideoFilter, VideoPrivacy, VideoState } from '@shared/models' | ||
2 | import { buildDirectionAndField, createSafeIn } from '@server/models/utils' | ||
3 | import { Model } from 'sequelize-typescript' | ||
4 | import { MUserAccountId, MUserId } from '@server/types/models' | ||
5 | import validator from 'validator' | ||
6 | import { exists } from '@server/helpers/custom-validators/misc' | ||
7 | |||
8 | export type BuildVideosQueryOptions = { | ||
9 | attributes?: string[] | ||
10 | |||
11 | serverAccountId: number | ||
12 | followerActorId: number | ||
13 | includeLocalVideos: boolean | ||
14 | |||
15 | count: number | ||
16 | start: number | ||
17 | sort: string | ||
18 | |||
19 | nsfw?: boolean | ||
20 | filter?: VideoFilter | ||
21 | isLive?: boolean | ||
22 | |||
23 | categoryOneOf?: number[] | ||
24 | licenceOneOf?: number[] | ||
25 | languageOneOf?: string[] | ||
26 | tagsOneOf?: string[] | ||
27 | tagsAllOf?: string[] | ||
28 | |||
29 | withFiles?: boolean | ||
30 | |||
31 | accountId?: number | ||
32 | videoChannelId?: number | ||
33 | |||
34 | videoPlaylistId?: number | ||
35 | |||
36 | trendingAlgorithm?: string // best, hot, or any other algorithm implemented | ||
37 | trendingDays?: number | ||
38 | |||
39 | user?: MUserAccountId | ||
40 | historyOfUser?: MUserId | ||
41 | |||
42 | startDate?: string // ISO 8601 | ||
43 | endDate?: string // ISO 8601 | ||
44 | originallyPublishedStartDate?: string | ||
45 | originallyPublishedEndDate?: string | ||
46 | |||
47 | durationMin?: number // seconds | ||
48 | durationMax?: number // seconds | ||
49 | |||
50 | search?: string | ||
51 | |||
52 | isCount?: boolean | ||
53 | |||
54 | group?: string | ||
55 | having?: string | ||
56 | } | ||
57 | |||
58 | function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions) { | ||
59 | const and: string[] = [] | ||
60 | const joins: string[] = [] | ||
61 | const replacements: any = {} | ||
62 | const cte: string[] = [] | ||
63 | |||
64 | let attributes: string[] = options.attributes || [ '"video"."id"' ] | ||
65 | let group = options.group || '' | ||
66 | const having = options.having || '' | ||
67 | |||
68 | joins.push( | ||
69 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId"' + | ||
70 | 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId"' + | ||
71 | 'INNER JOIN "actor" "accountActor" ON "account"."actorId" = "accountActor"."id"' | ||
72 | ) | ||
73 | |||
74 | and.push('"video"."id" NOT IN (SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")') | ||
75 | |||
76 | if (options.serverAccountId) { | ||
77 | const blockerIds = [ options.serverAccountId ] | ||
78 | if (options.user) blockerIds.push(options.user.Account.id) | ||
79 | |||
80 | const inClause = createSafeIn(model, blockerIds) | ||
81 | |||
82 | and.push( | ||
83 | 'NOT EXISTS (' + | ||
84 | ' SELECT 1 FROM "accountBlocklist" ' + | ||
85 | ' WHERE "accountBlocklist"."accountId" IN (' + inClause + ') ' + | ||
86 | ' AND "accountBlocklist"."targetAccountId" = "account"."id" ' + | ||
87 | ')' + | ||
88 | 'AND NOT EXISTS (' + | ||
89 | ' SELECT 1 FROM "serverBlocklist" WHERE "serverBlocklist"."accountId" IN (' + inClause + ') ' + | ||
90 | ' AND "serverBlocklist"."targetServerId" = "accountActor"."serverId"' + | ||
91 | ')' | ||
92 | ) | ||
93 | } | ||
94 | |||
95 | // Only list public/published videos | ||
96 | if (!options.filter || (options.filter !== 'all-local' && options.filter !== 'all')) { | ||
97 | and.push( | ||
98 | `("video"."state" = ${VideoState.PUBLISHED} OR ` + | ||
99 | `("video"."state" = ${VideoState.TO_TRANSCODE} AND "video"."waitTranscoding" IS false))` | ||
100 | ) | ||
101 | |||
102 | if (options.user) { | ||
103 | and.push( | ||
104 | `("video"."privacy" = ${VideoPrivacy.PUBLIC} OR "video"."privacy" = ${VideoPrivacy.INTERNAL})` | ||
105 | ) | ||
106 | } else { // Or only public videos | ||
107 | and.push( | ||
108 | `"video"."privacy" = ${VideoPrivacy.PUBLIC}` | ||
109 | ) | ||
110 | } | ||
111 | } | ||
112 | |||
113 | if (options.videoPlaylistId) { | ||
114 | joins.push( | ||
115 | 'INNER JOIN "videoPlaylistElement" "video"."id" = "videoPlaylistElement"."videoId" ' + | ||
116 | 'AND "videoPlaylistElement"."videoPlaylistId" = :videoPlaylistId' | ||
117 | ) | ||
118 | |||
119 | replacements.videoPlaylistId = options.videoPlaylistId | ||
120 | } | ||
121 | |||
122 | if (options.filter && (options.filter === 'local' || options.filter === 'all-local')) { | ||
123 | and.push('"video"."remote" IS FALSE') | ||
124 | } | ||
125 | |||
126 | if (options.accountId) { | ||
127 | and.push('"account"."id" = :accountId') | ||
128 | replacements.accountId = options.accountId | ||
129 | } | ||
130 | |||
131 | if (options.videoChannelId) { | ||
132 | and.push('"videoChannel"."id" = :videoChannelId') | ||
133 | replacements.videoChannelId = options.videoChannelId | ||
134 | } | ||
135 | |||
136 | if (options.followerActorId) { | ||
137 | let query = | ||
138 | '(' + | ||
139 | ' EXISTS (' + | ||
140 | ' SELECT 1 FROM "videoShare" ' + | ||
141 | ' INNER JOIN "actorFollow" "actorFollowShare" ON "actorFollowShare"."targetActorId" = "videoShare"."actorId" ' + | ||
142 | ' AND "actorFollowShare"."actorId" = :followerActorId AND "actorFollowShare"."state" = \'accepted\' ' + | ||
143 | ' WHERE "videoShare"."videoId" = "video"."id"' + | ||
144 | ' )' + | ||
145 | ' OR' + | ||
146 | ' EXISTS (' + | ||
147 | ' SELECT 1 from "actorFollow" ' + | ||
148 | ' WHERE "actorFollow"."targetActorId" = "videoChannel"."actorId" AND "actorFollow"."actorId" = :followerActorId ' + | ||
149 | ' AND "actorFollow"."state" = \'accepted\'' + | ||
150 | ' )' | ||
151 | |||
152 | if (options.includeLocalVideos) { | ||
153 | query += ' OR "video"."remote" IS FALSE' | ||
154 | } | ||
155 | |||
156 | query += ')' | ||
157 | |||
158 | and.push(query) | ||
159 | replacements.followerActorId = options.followerActorId | ||
160 | } | ||
161 | |||
162 | if (options.withFiles === true) { | ||
163 | and.push( | ||
164 | '(' + | ||
165 | ' EXISTS (SELECT 1 FROM "videoFile" WHERE "videoFile"."videoId" = "video"."id") ' + | ||
166 | ' OR EXISTS (' + | ||
167 | ' SELECT 1 FROM "videoStreamingPlaylist" ' + | ||
168 | ' INNER JOIN "videoFile" ON "videoFile"."videoStreamingPlaylistId" = "videoStreamingPlaylist"."id" ' + | ||
169 | ' WHERE "videoStreamingPlaylist"."videoId" = "video"."id"' + | ||
170 | ' )' + | ||
171 | ')' | ||
172 | ) | ||
173 | } | ||
174 | |||
175 | if (options.tagsOneOf) { | ||
176 | const tagsOneOfLower = options.tagsOneOf.map(t => t.toLowerCase()) | ||
177 | |||
178 | and.push( | ||
179 | 'EXISTS (' + | ||
180 | ' SELECT 1 FROM "videoTag" ' + | ||
181 | ' INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + | ||
182 | ' WHERE lower("tag"."name") IN (' + createSafeIn(model, tagsOneOfLower) + ') ' + | ||
183 | ' AND "video"."id" = "videoTag"."videoId"' + | ||
184 | ')' | ||
185 | ) | ||
186 | } | ||
187 | |||
188 | if (options.tagsAllOf) { | ||
189 | const tagsAllOfLower = options.tagsAllOf.map(t => t.toLowerCase()) | ||
190 | |||
191 | and.push( | ||
192 | 'EXISTS (' + | ||
193 | ' SELECT 1 FROM "videoTag" ' + | ||
194 | ' INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + | ||
195 | ' WHERE lower("tag"."name") IN (' + createSafeIn(model, tagsAllOfLower) + ') ' + | ||
196 | ' AND "video"."id" = "videoTag"."videoId" ' + | ||
197 | ' GROUP BY "videoTag"."videoId" HAVING COUNT(*) = ' + tagsAllOfLower.length + | ||
198 | ')' | ||
199 | ) | ||
200 | } | ||
201 | |||
202 | if (options.nsfw === true) { | ||
203 | and.push('"video"."nsfw" IS TRUE') | ||
204 | } else if (options.nsfw === false) { | ||
205 | and.push('"video"."nsfw" IS FALSE') | ||
206 | } | ||
207 | |||
208 | if (options.isLive === true) { | ||
209 | and.push('"video"."isLive" IS TRUE') | ||
210 | } else if (options.isLive === false) { | ||
211 | and.push('"video"."isLive" IS FALSE') | ||
212 | } | ||
213 | |||
214 | if (options.categoryOneOf) { | ||
215 | and.push('"video"."category" IN (:categoryOneOf)') | ||
216 | replacements.categoryOneOf = options.categoryOneOf | ||
217 | } | ||
218 | |||
219 | if (options.licenceOneOf) { | ||
220 | and.push('"video"."licence" IN (:licenceOneOf)') | ||
221 | replacements.licenceOneOf = options.licenceOneOf | ||
222 | } | ||
223 | |||
224 | if (options.languageOneOf) { | ||
225 | const languages = options.languageOneOf.filter(l => l && l !== '_unknown') | ||
226 | const languagesQueryParts: string[] = [] | ||
227 | |||
228 | if (languages.length !== 0) { | ||
229 | languagesQueryParts.push('"video"."language" IN (:languageOneOf)') | ||
230 | replacements.languageOneOf = languages | ||
231 | |||
232 | languagesQueryParts.push( | ||
233 | 'EXISTS (' + | ||
234 | ' SELECT 1 FROM "videoCaption" WHERE "videoCaption"."language" ' + | ||
235 | ' IN (' + createSafeIn(model, languages) + ') AND ' + | ||
236 | ' "videoCaption"."videoId" = "video"."id"' + | ||
237 | ')' | ||
238 | ) | ||
239 | } | ||
240 | |||
241 | if (options.languageOneOf.includes('_unknown')) { | ||
242 | languagesQueryParts.push('"video"."language" IS NULL') | ||
243 | } | ||
244 | |||
245 | if (languagesQueryParts.length !== 0) { | ||
246 | and.push('(' + languagesQueryParts.join(' OR ') + ')') | ||
247 | } | ||
248 | } | ||
249 | |||
250 | // We don't exclude results in this so if we do a count we don't need to add this complex clause | ||
251 | if (options.isCount !== true) { | ||
252 | if (options.trendingDays) { | ||
253 | const viewsGteDate = new Date(new Date().getTime() - (24 * 3600 * 1000) * options.trendingDays) | ||
254 | |||
255 | joins.push('LEFT JOIN "videoView" ON "video"."id" = "videoView"."videoId" AND "videoView"."startDate" >= :viewsGteDate') | ||
256 | replacements.viewsGteDate = viewsGteDate | ||
257 | |||
258 | attributes.push('COALESCE(SUM("videoView"."views"), 0) AS "score"') | ||
259 | |||
260 | group = 'GROUP BY "video"."id"' | ||
261 | } else if ([ 'best', 'hot' ].includes(options.trendingAlgorithm)) { | ||
262 | /** | ||
263 | * "Hotness" is a measure based on absolute view/comment/like/dislike numbers, | ||
264 | * with fixed weights only applied to their log values. | ||
265 | * | ||
266 | * This algorithm gives little chance for an old video to have a good score, | ||
267 | * for which recent spikes in interactions could be a sign of "hotness" and | ||
268 | * justify a better score. However there are multiple ways to achieve that | ||
269 | * goal, which is left for later. Yes, this is a TODO :) | ||
270 | * | ||
271 | * notes: | ||
272 | * - weights and base score are in number of half-days. | ||
273 | * - all comments are counted, regardless of being written by the video author or not | ||
274 | * see https://github.com/reddit-archive/reddit/blob/master/r2/r2/lib/db/_sorts.pyx#L47-L58 | ||
275 | * - we have less interactions than on reddit, so multiply weights by an arbitrary factor | ||
276 | */ | ||
277 | const weights = { | ||
278 | like: 3 * 50, | ||
279 | dislike: -3 * 50, | ||
280 | view: Math.floor((1 / 3) * 50), | ||
281 | comment: 2 * 50, // a comment takes more time than a like to do, but can be done multiple times | ||
282 | history: -2 * 50 | ||
283 | } | ||
284 | |||
285 | joins.push('LEFT JOIN "videoComment" ON "video"."id" = "videoComment"."videoId"') | ||
286 | |||
287 | let attribute = | ||
288 | `LOG(GREATEST(1, "video"."likes" - 1)) * ${weights.like} ` + // likes (+) | ||
289 | `+ LOG(GREATEST(1, "video"."dislikes" - 1)) * ${weights.dislike} ` + // dislikes (-) | ||
290 | `+ LOG("video"."views" + 1) * ${weights.view} ` + // views (+) | ||
291 | `+ LOG(GREATEST(1, COUNT(DISTINCT "videoComment"."id"))) * ${weights.comment} ` + // comments (+) | ||
292 | '+ (SELECT (EXTRACT(epoch FROM "video"."publishedAt") - 1446156582) / 47000) ' // base score (in number of half-days) | ||
293 | |||
294 | if (options.trendingAlgorithm === 'best' && options.user) { | ||
295 | joins.push( | ||
296 | 'LEFT JOIN "userVideoHistory" ON "video"."id" = "userVideoHistory"."videoId" AND "userVideoHistory"."userId" = :bestUser' | ||
297 | ) | ||
298 | replacements.bestUser = options.user.id | ||
299 | |||
300 | attribute += `+ POWER(COUNT(DISTINCT "userVideoHistory"."id"), 2.0) * ${weights.history} ` | ||
301 | } | ||
302 | |||
303 | attribute += 'AS "score"' | ||
304 | attributes.push(attribute) | ||
305 | |||
306 | group = 'GROUP BY "video"."id"' | ||
307 | } | ||
308 | } | ||
309 | |||
310 | if (options.historyOfUser) { | ||
311 | joins.push('INNER JOIN "userVideoHistory" ON "video"."id" = "userVideoHistory"."videoId"') | ||
312 | |||
313 | and.push('"userVideoHistory"."userId" = :historyOfUser') | ||
314 | replacements.historyOfUser = options.historyOfUser.id | ||
315 | } | ||
316 | |||
317 | if (options.startDate) { | ||
318 | and.push('"video"."publishedAt" >= :startDate') | ||
319 | replacements.startDate = options.startDate | ||
320 | } | ||
321 | |||
322 | if (options.endDate) { | ||
323 | and.push('"video"."publishedAt" <= :endDate') | ||
324 | replacements.endDate = options.endDate | ||
325 | } | ||
326 | |||
327 | if (options.originallyPublishedStartDate) { | ||
328 | and.push('"video"."originallyPublishedAt" >= :originallyPublishedStartDate') | ||
329 | replacements.originallyPublishedStartDate = options.originallyPublishedStartDate | ||
330 | } | ||
331 | |||
332 | if (options.originallyPublishedEndDate) { | ||
333 | and.push('"video"."originallyPublishedAt" <= :originallyPublishedEndDate') | ||
334 | replacements.originallyPublishedEndDate = options.originallyPublishedEndDate | ||
335 | } | ||
336 | |||
337 | if (options.durationMin) { | ||
338 | and.push('"video"."duration" >= :durationMin') | ||
339 | replacements.durationMin = options.durationMin | ||
340 | } | ||
341 | |||
342 | if (options.durationMax) { | ||
343 | and.push('"video"."duration" <= :durationMax') | ||
344 | replacements.durationMax = options.durationMax | ||
345 | } | ||
346 | |||
347 | if (options.search) { | ||
348 | const escapedSearch = model.sequelize.escape(options.search) | ||
349 | const escapedLikeSearch = model.sequelize.escape('%' + options.search + '%') | ||
350 | |||
351 | cte.push( | ||
352 | '"trigramSearch" AS (' + | ||
353 | ' SELECT "video"."id", ' + | ||
354 | ` similarity(lower(immutable_unaccent("video"."name")), lower(immutable_unaccent(${escapedSearch}))) as similarity ` + | ||
355 | ' FROM "video" ' + | ||
356 | ' WHERE lower(immutable_unaccent("video"."name")) % lower(immutable_unaccent(' + escapedSearch + ')) OR ' + | ||
357 | ' lower(immutable_unaccent("video"."name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))' + | ||
358 | ')' | ||
359 | ) | ||
360 | |||
361 | joins.push('LEFT JOIN "trigramSearch" ON "video"."id" = "trigramSearch"."id"') | ||
362 | |||
363 | let base = '(' + | ||
364 | ' "trigramSearch"."id" IS NOT NULL OR ' + | ||
365 | ' EXISTS (' + | ||
366 | ' SELECT 1 FROM "videoTag" ' + | ||
367 | ' INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + | ||
368 | ` WHERE lower("tag"."name") = ${escapedSearch} ` + | ||
369 | ' AND "video"."id" = "videoTag"."videoId"' + | ||
370 | ' )' | ||
371 | |||
372 | if (validator.isUUID(options.search)) { | ||
373 | base += ` OR "video"."uuid" = ${escapedSearch}` | ||
374 | } | ||
375 | |||
376 | base += ')' | ||
377 | and.push(base) | ||
378 | |||
379 | attributes.push(`COALESCE("trigramSearch"."similarity", 0) as similarity`) | ||
380 | } else { | ||
381 | attributes.push('0 as similarity') | ||
382 | } | ||
383 | |||
384 | if (options.isCount === true) attributes = [ 'COUNT(*) as "total"' ] | ||
385 | |||
386 | let suffix = '' | ||
387 | let order = '' | ||
388 | if (options.isCount !== true) { | ||
389 | |||
390 | if (exists(options.sort)) { | ||
391 | if (options.sort === '-originallyPublishedAt' || options.sort === 'originallyPublishedAt') { | ||
392 | attributes.push('COALESCE("video"."originallyPublishedAt", "video"."publishedAt") AS "publishedAtForOrder"') | ||
393 | } | ||
394 | |||
395 | order = buildOrder(options.sort) | ||
396 | suffix += `${order} ` | ||
397 | } | ||
398 | |||
399 | if (exists(options.count)) { | ||
400 | const count = parseInt(options.count + '', 10) | ||
401 | suffix += `LIMIT ${count} ` | ||
402 | } | ||
403 | |||
404 | if (exists(options.start)) { | ||
405 | const start = parseInt(options.start + '', 10) | ||
406 | suffix += `OFFSET ${start} ` | ||
407 | } | ||
408 | } | ||
409 | |||
410 | const cteString = cte.length !== 0 | ||
411 | ? `WITH ${cte.join(', ')} ` | ||
412 | : '' | ||
413 | |||
414 | const query = cteString + | ||
415 | 'SELECT ' + attributes.join(', ') + ' ' + | ||
416 | 'FROM "video" ' + joins.join(' ') + ' ' + | ||
417 | 'WHERE ' + and.join(' AND ') + ' ' + | ||
418 | group + ' ' + | ||
419 | having + ' ' + | ||
420 | suffix | ||
421 | |||
422 | return { query, replacements, order } | ||
423 | } | ||
424 | |||
425 | function buildOrder (value: string) { | ||
426 | const { direction, field } = buildDirectionAndField(value) | ||
427 | if (field.match(/^[a-zA-Z."]+$/) === null) throw new Error('Invalid sort column ' + field) | ||
428 | |||
429 | if (field.toLowerCase() === 'random') return 'ORDER BY RANDOM()' | ||
430 | |||
431 | if ([ 'trending', 'hot', 'best' ].includes(field.toLowerCase())) { // Sort by aggregation | ||
432 | return `ORDER BY "score" ${direction}, "video"."views" ${direction}` | ||
433 | } | ||
434 | |||
435 | let firstSort: string | ||
436 | |||
437 | if (field.toLowerCase() === 'match') { // Search | ||
438 | firstSort = '"similarity"' | ||
439 | } else if (field === 'originallyPublishedAt') { | ||
440 | firstSort = '"publishedAtForOrder"' | ||
441 | } else if (field.includes('.')) { | ||
442 | firstSort = field | ||
443 | } else { | ||
444 | firstSort = `"video"."${field}"` | ||
445 | } | ||
446 | |||
447 | return `ORDER BY ${firstSort} ${direction}, "video"."id" ASC` | ||
448 | } | ||
449 | |||
450 | function wrapForAPIResults (baseQuery: string, replacements: any, options: BuildVideosQueryOptions, order: string) { | ||
451 | const attributes = { | ||
452 | '"video".*': '', | ||
453 | '"VideoChannel"."id"': '"VideoChannel.id"', | ||
454 | '"VideoChannel"."name"': '"VideoChannel.name"', | ||
455 | '"VideoChannel"."description"': '"VideoChannel.description"', | ||
456 | '"VideoChannel"."actorId"': '"VideoChannel.actorId"', | ||
457 | '"VideoChannel->Actor"."id"': '"VideoChannel.Actor.id"', | ||
458 | '"VideoChannel->Actor"."preferredUsername"': '"VideoChannel.Actor.preferredUsername"', | ||
459 | '"VideoChannel->Actor"."url"': '"VideoChannel.Actor.url"', | ||
460 | '"VideoChannel->Actor"."serverId"': '"VideoChannel.Actor.serverId"', | ||
461 | '"VideoChannel->Actor"."avatarId"': '"VideoChannel.Actor.avatarId"', | ||
462 | '"VideoChannel->Account"."id"': '"VideoChannel.Account.id"', | ||
463 | '"VideoChannel->Account"."name"': '"VideoChannel.Account.name"', | ||
464 | '"VideoChannel->Account->Actor"."id"': '"VideoChannel.Account.Actor.id"', | ||
465 | '"VideoChannel->Account->Actor"."preferredUsername"': '"VideoChannel.Account.Actor.preferredUsername"', | ||
466 | '"VideoChannel->Account->Actor"."url"': '"VideoChannel.Account.Actor.url"', | ||
467 | '"VideoChannel->Account->Actor"."serverId"': '"VideoChannel.Account.Actor.serverId"', | ||
468 | '"VideoChannel->Account->Actor"."avatarId"': '"VideoChannel.Account.Actor.avatarId"', | ||
469 | '"VideoChannel->Actor->Server"."id"': '"VideoChannel.Actor.Server.id"', | ||
470 | '"VideoChannel->Actor->Server"."host"': '"VideoChannel.Actor.Server.host"', | ||
471 | '"VideoChannel->Actor->Avatar"."id"': '"VideoChannel.Actor.Avatar.id"', | ||
472 | '"VideoChannel->Actor->Avatar"."filename"': '"VideoChannel.Actor.Avatar.filename"', | ||
473 | '"VideoChannel->Actor->Avatar"."fileUrl"': '"VideoChannel.Actor.Avatar.fileUrl"', | ||
474 | '"VideoChannel->Actor->Avatar"."onDisk"': '"VideoChannel.Actor.Avatar.onDisk"', | ||
475 | '"VideoChannel->Actor->Avatar"."createdAt"': '"VideoChannel.Actor.Avatar.createdAt"', | ||
476 | '"VideoChannel->Actor->Avatar"."updatedAt"': '"VideoChannel.Actor.Avatar.updatedAt"', | ||
477 | '"VideoChannel->Account->Actor->Server"."id"': '"VideoChannel.Account.Actor.Server.id"', | ||
478 | '"VideoChannel->Account->Actor->Server"."host"': '"VideoChannel.Account.Actor.Server.host"', | ||
479 | '"VideoChannel->Account->Actor->Avatar"."id"': '"VideoChannel.Account.Actor.Avatar.id"', | ||
480 | '"VideoChannel->Account->Actor->Avatar"."filename"': '"VideoChannel.Account.Actor.Avatar.filename"', | ||
481 | '"VideoChannel->Account->Actor->Avatar"."fileUrl"': '"VideoChannel.Account.Actor.Avatar.fileUrl"', | ||
482 | '"VideoChannel->Account->Actor->Avatar"."onDisk"': '"VideoChannel.Account.Actor.Avatar.onDisk"', | ||
483 | '"VideoChannel->Account->Actor->Avatar"."createdAt"': '"VideoChannel.Account.Actor.Avatar.createdAt"', | ||
484 | '"VideoChannel->Account->Actor->Avatar"."updatedAt"': '"VideoChannel.Account.Actor.Avatar.updatedAt"', | ||
485 | '"Thumbnails"."id"': '"Thumbnails.id"', | ||
486 | '"Thumbnails"."type"': '"Thumbnails.type"', | ||
487 | '"Thumbnails"."filename"': '"Thumbnails.filename"' | ||
488 | } | ||
489 | |||
490 | const joins = [ | ||
491 | 'INNER JOIN "video" ON "tmp"."id" = "video"."id"', | ||
492 | |||
493 | 'INNER JOIN "videoChannel" AS "VideoChannel" ON "video"."channelId" = "VideoChannel"."id"', | ||
494 | 'INNER JOIN "actor" AS "VideoChannel->Actor" ON "VideoChannel"."actorId" = "VideoChannel->Actor"."id"', | ||
495 | 'INNER JOIN "account" AS "VideoChannel->Account" ON "VideoChannel"."accountId" = "VideoChannel->Account"."id"', | ||
496 | 'INNER JOIN "actor" AS "VideoChannel->Account->Actor" ON "VideoChannel->Account"."actorId" = "VideoChannel->Account->Actor"."id"', | ||
497 | |||
498 | 'LEFT OUTER JOIN "server" AS "VideoChannel->Actor->Server" ON "VideoChannel->Actor"."serverId" = "VideoChannel->Actor->Server"."id"', | ||
499 | 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Actor->Avatar" ' + | ||
500 | 'ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"', | ||
501 | |||
502 | 'LEFT OUTER JOIN "server" AS "VideoChannel->Account->Actor->Server" ' + | ||
503 | 'ON "VideoChannel->Account->Actor"."serverId" = "VideoChannel->Account->Actor->Server"."id"', | ||
504 | |||
505 | 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Account->Actor->Avatar" ' + | ||
506 | 'ON "VideoChannel->Account->Actor"."avatarId" = "VideoChannel->Account->Actor->Avatar"."id"', | ||
507 | |||
508 | 'LEFT OUTER JOIN "thumbnail" AS "Thumbnails" ON "video"."id" = "Thumbnails"."videoId"' | ||
509 | ] | ||
510 | |||
511 | if (options.withFiles) { | ||
512 | joins.push('LEFT JOIN "videoFile" AS "VideoFiles" ON "VideoFiles"."videoId" = "video"."id"') | ||
513 | |||
514 | joins.push('LEFT JOIN "videoStreamingPlaylist" AS "VideoStreamingPlaylists" ON "VideoStreamingPlaylists"."videoId" = "video"."id"') | ||
515 | joins.push( | ||
516 | 'LEFT JOIN "videoFile" AS "VideoStreamingPlaylists->VideoFiles" ' + | ||
517 | 'ON "VideoStreamingPlaylists->VideoFiles"."videoStreamingPlaylistId" = "VideoStreamingPlaylists"."id"' | ||
518 | ) | ||
519 | |||
520 | Object.assign(attributes, { | ||
521 | '"VideoFiles"."id"': '"VideoFiles.id"', | ||
522 | '"VideoFiles"."createdAt"': '"VideoFiles.createdAt"', | ||
523 | '"VideoFiles"."updatedAt"': '"VideoFiles.updatedAt"', | ||
524 | '"VideoFiles"."resolution"': '"VideoFiles.resolution"', | ||
525 | '"VideoFiles"."size"': '"VideoFiles.size"', | ||
526 | '"VideoFiles"."extname"': '"VideoFiles.extname"', | ||
527 | '"VideoFiles"."filename"': '"VideoFiles.filename"', | ||
528 | '"VideoFiles"."fileUrl"': '"VideoFiles.fileUrl"', | ||
529 | '"VideoFiles"."torrentFilename"': '"VideoFiles.torrentFilename"', | ||
530 | '"VideoFiles"."torrentUrl"': '"VideoFiles.torrentUrl"', | ||
531 | '"VideoFiles"."infoHash"': '"VideoFiles.infoHash"', | ||
532 | '"VideoFiles"."fps"': '"VideoFiles.fps"', | ||
533 | '"VideoFiles"."videoId"': '"VideoFiles.videoId"', | ||
534 | |||
535 | '"VideoStreamingPlaylists"."id"': '"VideoStreamingPlaylists.id"', | ||
536 | '"VideoStreamingPlaylists"."playlistUrl"': '"VideoStreamingPlaylists.playlistUrl"', | ||
537 | '"VideoStreamingPlaylists"."type"': '"VideoStreamingPlaylists.type"', | ||
538 | '"VideoStreamingPlaylists->VideoFiles"."id"': '"VideoStreamingPlaylists.VideoFiles.id"', | ||
539 | '"VideoStreamingPlaylists->VideoFiles"."createdAt"': '"VideoStreamingPlaylists.VideoFiles.createdAt"', | ||
540 | '"VideoStreamingPlaylists->VideoFiles"."updatedAt"': '"VideoStreamingPlaylists.VideoFiles.updatedAt"', | ||
541 | '"VideoStreamingPlaylists->VideoFiles"."resolution"': '"VideoStreamingPlaylists.VideoFiles.resolution"', | ||
542 | '"VideoStreamingPlaylists->VideoFiles"."size"': '"VideoStreamingPlaylists.VideoFiles.size"', | ||
543 | '"VideoStreamingPlaylists->VideoFiles"."extname"': '"VideoStreamingPlaylists.VideoFiles.extname"', | ||
544 | '"VideoStreamingPlaylists->VideoFiles"."filename"': '"VideoStreamingPlaylists.VideoFiles.filename"', | ||
545 | '"VideoStreamingPlaylists->VideoFiles"."fileUrl"': '"VideoStreamingPlaylists.VideoFiles.fileUrl"', | ||
546 | '"VideoStreamingPlaylists->VideoFiles"."torrentFilename"': '"VideoStreamingPlaylists.VideoFiles.torrentFilename"', | ||
547 | '"VideoStreamingPlaylists->VideoFiles"."torrentUrl"': '"VideoStreamingPlaylists.VideoFiles.torrentUrl"', | ||
548 | '"VideoStreamingPlaylists->VideoFiles"."infoHash"': '"VideoStreamingPlaylists.VideoFiles.infoHash"', | ||
549 | '"VideoStreamingPlaylists->VideoFiles"."fps"': '"VideoStreamingPlaylists.VideoFiles.fps"', | ||
550 | '"VideoStreamingPlaylists->VideoFiles"."videoStreamingPlaylistId"': '"VideoStreamingPlaylists.VideoFiles.videoStreamingPlaylistId"', | ||
551 | '"VideoStreamingPlaylists->VideoFiles"."videoId"': '"VideoStreamingPlaylists.VideoFiles.videoId"' | ||
552 | }) | ||
553 | } | ||
554 | |||
555 | if (options.user) { | ||
556 | joins.push( | ||
557 | 'LEFT OUTER JOIN "userVideoHistory" ' + | ||
558 | 'ON "video"."id" = "userVideoHistory"."videoId" AND "userVideoHistory"."userId" = :userVideoHistoryId' | ||
559 | ) | ||
560 | replacements.userVideoHistoryId = options.user.id | ||
561 | |||
562 | Object.assign(attributes, { | ||
563 | '"userVideoHistory"."id"': '"userVideoHistory.id"', | ||
564 | '"userVideoHistory"."currentTime"': '"userVideoHistory.currentTime"' | ||
565 | }) | ||
566 | } | ||
567 | |||
568 | if (options.videoPlaylistId) { | ||
569 | joins.push( | ||
570 | 'INNER JOIN "videoPlaylistElement" as "VideoPlaylistElement" ON "videoPlaylistElement"."videoId" = "video"."id" ' + | ||
571 | 'AND "VideoPlaylistElement"."videoPlaylistId" = :videoPlaylistId' | ||
572 | ) | ||
573 | replacements.videoPlaylistId = options.videoPlaylistId | ||
574 | |||
575 | Object.assign(attributes, { | ||
576 | '"VideoPlaylistElement"."createdAt"': '"VideoPlaylistElement.createdAt"', | ||
577 | '"VideoPlaylistElement"."updatedAt"': '"VideoPlaylistElement.updatedAt"', | ||
578 | '"VideoPlaylistElement"."url"': '"VideoPlaylistElement.url"', | ||
579 | '"VideoPlaylistElement"."position"': '"VideoPlaylistElement.position"', | ||
580 | '"VideoPlaylistElement"."startTimestamp"': '"VideoPlaylistElement.startTimestamp"', | ||
581 | '"VideoPlaylistElement"."stopTimestamp"': '"VideoPlaylistElement.stopTimestamp"', | ||
582 | '"VideoPlaylistElement"."videoPlaylistId"': '"VideoPlaylistElement.videoPlaylistId"' | ||
583 | }) | ||
584 | } | ||
585 | |||
586 | const select = 'SELECT ' + Object.keys(attributes).map(key => { | ||
587 | const value = attributes[key] | ||
588 | if (value) return `${key} AS ${value}` | ||
589 | |||
590 | return key | ||
591 | }).join(', ') | ||
592 | |||
593 | return `${select} FROM (${baseQuery}) AS "tmp" ${joins.join(' ')} ${order}` | ||
594 | } | ||
595 | |||
596 | export { | ||
597 | buildListQuery, | ||
598 | wrapForAPIResults | ||
599 | } | ||
diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts index 5059c1fa6..505c305e2 100644 --- a/server/models/video/video-share.ts +++ b/server/models/video/video-share.ts | |||
@@ -1,10 +1,11 @@ | |||
1 | import { literal, Op, QueryTypes, Transaction } from 'sequelize' | 1 | import { literal, Op, QueryTypes, Transaction } from 'sequelize' |
2 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { AttributesOnly } from '@shared/core-utils' | ||
3 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 4 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
4 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | 5 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' |
5 | import { MActorDefault } from '../../types/models' | 6 | import { MActorDefault } from '../../types/models' |
6 | import { MVideoShareActor, MVideoShareFull } from '../../types/models/video' | 7 | import { MVideoShareActor, MVideoShareFull } from '../../types/models/video' |
7 | import { ActorModel } from '../activitypub/actor' | 8 | import { ActorModel } from '../actor/actor' |
8 | import { buildLocalActorIdsIn, throwIfNotValid } from '../utils' | 9 | import { buildLocalActorIdsIn, throwIfNotValid } from '../utils' |
9 | import { VideoModel } from './video' | 10 | import { VideoModel } from './video' |
10 | 11 | ||
@@ -50,7 +51,7 @@ enum ScopeNames { | |||
50 | } | 51 | } |
51 | ] | 52 | ] |
52 | }) | 53 | }) |
53 | export class VideoShareModel extends Model { | 54 | export class VideoShareModel extends Model<Partial<AttributesOnly<VideoShareModel>>> { |
54 | 55 | ||
55 | @AllowNull(false) | 56 | @AllowNull(false) |
56 | @Is('VideoShareUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url')) | 57 | @Is('VideoShareUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url')) |
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts index c9375b433..d627e8c9d 100644 --- a/server/models/video/video-streaming-playlist.ts +++ b/server/models/video/video-streaming-playlist.ts | |||
@@ -13,6 +13,7 @@ import { CONSTRAINTS_FIELDS, MEMOIZE_LENGTH, MEMOIZE_TTL, P2P_MEDIA_LOADER_PEER_ | |||
13 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 13 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
14 | import { throwIfNotValid } from '../utils' | 14 | import { throwIfNotValid } from '../utils' |
15 | import { VideoModel } from './video' | 15 | import { VideoModel } from './video' |
16 | import { AttributesOnly } from '@shared/core-utils' | ||
16 | 17 | ||
17 | @Table({ | 18 | @Table({ |
18 | tableName: 'videoStreamingPlaylist', | 19 | tableName: 'videoStreamingPlaylist', |
@@ -30,7 +31,7 @@ import { VideoModel } from './video' | |||
30 | } | 31 | } |
31 | ] | 32 | ] |
32 | }) | 33 | }) |
33 | export class VideoStreamingPlaylistModel extends Model { | 34 | export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<VideoStreamingPlaylistModel>>> { |
34 | @CreatedAt | 35 | @CreatedAt |
35 | createdAt: Date | 36 | createdAt: Date |
36 | 37 | ||
diff --git a/server/models/video/video-tag.ts b/server/models/video/video-tag.ts index 5052b8c4d..1285d375b 100644 --- a/server/models/video/video-tag.ts +++ b/server/models/video/video-tag.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { AttributesOnly } from '@shared/core-utils' | ||
2 | import { TagModel } from './tag' | 3 | import { TagModel } from './tag' |
3 | import { VideoModel } from './video' | 4 | import { VideoModel } from './video' |
4 | 5 | ||
@@ -13,7 +14,7 @@ import { VideoModel } from './video' | |||
13 | } | 14 | } |
14 | ] | 15 | ] |
15 | }) | 16 | }) |
16 | export class VideoTagModel extends Model { | 17 | export class VideoTagModel extends Model<Partial<AttributesOnly<VideoTagModel>>> { |
17 | @CreatedAt | 18 | @CreatedAt |
18 | createdAt: Date | 19 | createdAt: Date |
19 | 20 | ||
diff --git a/server/models/video/video-view.ts b/server/models/video/video-view.ts index 992cf258a..dfc6296ce 100644 --- a/server/models/video/video-view.ts +++ b/server/models/video/video-view.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, Model, Table } from 'sequelize-typescript' | 2 | import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, Model, Table } from 'sequelize-typescript' |
3 | import { AttributesOnly } from '@shared/core-utils' | ||
2 | import { VideoModel } from './video' | 4 | import { VideoModel } from './video' |
3 | import * as Sequelize from 'sequelize' | ||
4 | 5 | ||
5 | @Table({ | 6 | @Table({ |
6 | tableName: 'videoView', | 7 | tableName: 'videoView', |
@@ -14,7 +15,7 @@ import * as Sequelize from 'sequelize' | |||
14 | } | 15 | } |
15 | ] | 16 | ] |
16 | }) | 17 | }) |
17 | export class VideoViewModel extends Model { | 18 | export class VideoViewModel extends Model<Partial<AttributesOnly<VideoViewModel>>> { |
18 | @CreatedAt | 19 | @CreatedAt |
19 | createdAt: Date | 20 | createdAt: Date |
20 | 21 | ||
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 18afba1ba..1e5648a36 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { remove } from 'fs-extra' | 2 | import { remove } from 'fs-extra' |
3 | import { maxBy, minBy, pick } from 'lodash' | 3 | import { maxBy, minBy } from 'lodash' |
4 | import { join } from 'path' | 4 | import { join } from 'path' |
5 | import { FindOptions, Includeable, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' | 5 | import { FindOptions, Includeable, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' |
6 | import { | 6 | import { |
@@ -27,10 +27,11 @@ import { | |||
27 | import { setAsUpdated } from '@server/helpers/database-utils' | 27 | import { setAsUpdated } from '@server/helpers/database-utils' |
28 | import { buildNSFWFilter } from '@server/helpers/express-utils' | 28 | import { buildNSFWFilter } from '@server/helpers/express-utils' |
29 | import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' | 29 | import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' |
30 | import { LiveManager } from '@server/lib/live-manager' | 30 | import { LiveManager } from '@server/lib/live/live-manager' |
31 | import { getHLSDirectory, getVideoFilePath } from '@server/lib/video-paths' | 31 | import { getHLSDirectory, getVideoFilePath } from '@server/lib/video-paths' |
32 | import { getServerActor } from '@server/models/application/application' | 32 | import { getServerActor } from '@server/models/application/application' |
33 | import { ModelCache } from '@server/models/model-cache' | 33 | import { ModelCache } from '@server/models/model-cache' |
34 | import { AttributesOnly } from '@shared/core-utils' | ||
34 | import { VideoFile } from '@shared/models/videos/video-file.model' | 35 | import { VideoFile } from '@shared/models/videos/video-file.model' |
35 | import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared' | 36 | import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared' |
36 | import { VideoObject } from '../../../shared/models/activitypub/objects' | 37 | import { VideoObject } from '../../../shared/models/activitypub/objects' |
@@ -42,11 +43,8 @@ import { peertubeTruncate } from '../../helpers/core-utils' | |||
42 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 43 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
43 | import { isBooleanValid } from '../../helpers/custom-validators/misc' | 44 | import { isBooleanValid } from '../../helpers/custom-validators/misc' |
44 | import { | 45 | import { |
45 | isVideoCategoryValid, | ||
46 | isVideoDescriptionValid, | 46 | isVideoDescriptionValid, |
47 | isVideoDurationValid, | 47 | isVideoDurationValid, |
48 | isVideoLanguageValid, | ||
49 | isVideoLicenceValid, | ||
50 | isVideoNameValid, | 48 | isVideoNameValid, |
51 | isVideoPrivacyValid, | 49 | isVideoPrivacyValid, |
52 | isVideoStateValid, | 50 | isVideoStateValid, |
@@ -55,19 +53,7 @@ import { | |||
55 | import { getVideoFileResolution } from '../../helpers/ffprobe-utils' | 53 | import { getVideoFileResolution } from '../../helpers/ffprobe-utils' |
56 | import { logger } from '../../helpers/logger' | 54 | import { logger } from '../../helpers/logger' |
57 | import { CONFIG } from '../../initializers/config' | 55 | import { CONFIG } from '../../initializers/config' |
58 | import { | 56 | import { ACTIVITY_PUB, API_VERSION, CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, STATIC_PATHS, WEBSERVER } from '../../initializers/constants' |
59 | ACTIVITY_PUB, | ||
60 | API_VERSION, | ||
61 | CONSTRAINTS_FIELDS, | ||
62 | LAZY_STATIC_PATHS, | ||
63 | STATIC_PATHS, | ||
64 | VIDEO_CATEGORIES, | ||
65 | VIDEO_LANGUAGES, | ||
66 | VIDEO_LICENCES, | ||
67 | VIDEO_PRIVACIES, | ||
68 | VIDEO_STATES, | ||
69 | WEBSERVER | ||
70 | } from '../../initializers/constants' | ||
71 | import { sendDeleteVideo } from '../../lib/activitypub/send' | 57 | import { sendDeleteVideo } from '../../lib/activitypub/send' |
72 | import { | 58 | import { |
73 | MChannel, | 59 | MChannel, |
@@ -87,29 +73,38 @@ import { | |||
87 | MVideoFormattableDetails, | 73 | MVideoFormattableDetails, |
88 | MVideoForUser, | 74 | MVideoForUser, |
89 | MVideoFullLight, | 75 | MVideoFullLight, |
90 | MVideoIdThumbnail, | 76 | MVideoId, |
91 | MVideoImmutable, | 77 | MVideoImmutable, |
92 | MVideoThumbnail, | 78 | MVideoThumbnail, |
93 | MVideoThumbnailBlacklist, | 79 | MVideoThumbnailBlacklist, |
94 | MVideoWithAllFiles, | 80 | MVideoWithAllFiles, |
95 | MVideoWithFile, | 81 | MVideoWithFile |
96 | MVideoWithRights | ||
97 | } from '../../types/models' | 82 | } from '../../types/models' |
98 | import { MThumbnail } from '../../types/models/video/thumbnail' | 83 | import { MThumbnail } from '../../types/models/video/thumbnail' |
99 | import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file' | 84 | import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file' |
100 | import { VideoAbuseModel } from '../abuse/video-abuse' | 85 | import { VideoAbuseModel } from '../abuse/video-abuse' |
101 | import { AccountModel } from '../account/account' | 86 | import { AccountModel } from '../account/account' |
102 | import { AccountVideoRateModel } from '../account/account-video-rate' | 87 | import { AccountVideoRateModel } from '../account/account-video-rate' |
103 | import { ActorImageModel } from '../account/actor-image' | 88 | import { ActorModel } from '../actor/actor' |
104 | import { UserModel } from '../account/user' | 89 | import { ActorImageModel } from '../actor/actor-image' |
105 | import { UserVideoHistoryModel } from '../account/user-video-history' | ||
106 | import { ActorModel } from '../activitypub/actor' | ||
107 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 90 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
108 | import { ServerModel } from '../server/server' | 91 | import { ServerModel } from '../server/server' |
109 | import { TrackerModel } from '../server/tracker' | 92 | import { TrackerModel } from '../server/tracker' |
110 | import { VideoTrackerModel } from '../server/video-tracker' | 93 | import { VideoTrackerModel } from '../server/video-tracker' |
94 | import { UserModel } from '../user/user' | ||
95 | import { UserVideoHistoryModel } from '../user/user-video-history' | ||
111 | import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils' | 96 | import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils' |
97 | import { | ||
98 | videoFilesModelToFormattedJSON, | ||
99 | VideoFormattingJSONOptions, | ||
100 | videoModelToActivityPubObject, | ||
101 | videoModelToFormattedDetailsJSON, | ||
102 | videoModelToFormattedJSON | ||
103 | } from './formatter/video-format-utils' | ||
112 | import { ScheduleVideoUpdateModel } from './schedule-video-update' | 104 | import { ScheduleVideoUpdateModel } from './schedule-video-update' |
105 | import { VideosModelGetQueryBuilder } from './sql/video-model-get-query-builder' | ||
106 | import { BuildVideosListQueryOptions, VideosIdListQueryBuilder } from './sql/videos-id-list-query-builder' | ||
107 | import { VideosModelListQueryBuilder } from './sql/videos-model-list-query-builder' | ||
113 | import { TagModel } from './tag' | 108 | import { TagModel } from './tag' |
114 | import { ThumbnailModel } from './thumbnail' | 109 | import { ThumbnailModel } from './thumbnail' |
115 | import { VideoBlacklistModel } from './video-blacklist' | 110 | import { VideoBlacklistModel } from './video-blacklist' |
@@ -117,37 +112,25 @@ import { VideoCaptionModel } from './video-caption' | |||
117 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' | 112 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' |
118 | import { VideoCommentModel } from './video-comment' | 113 | import { VideoCommentModel } from './video-comment' |
119 | import { VideoFileModel } from './video-file' | 114 | import { VideoFileModel } from './video-file' |
120 | import { | ||
121 | videoFilesModelToFormattedJSON, | ||
122 | VideoFormattingJSONOptions, | ||
123 | videoModelToActivityPubObject, | ||
124 | videoModelToFormattedDetailsJSON, | ||
125 | videoModelToFormattedJSON | ||
126 | } from './video-format-utils' | ||
127 | import { VideoImportModel } from './video-import' | 115 | import { VideoImportModel } from './video-import' |
128 | import { VideoLiveModel } from './video-live' | 116 | import { VideoLiveModel } from './video-live' |
129 | import { VideoPlaylistElementModel } from './video-playlist-element' | 117 | import { VideoPlaylistElementModel } from './video-playlist-element' |
130 | import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder' | ||
131 | import { VideoShareModel } from './video-share' | 118 | import { VideoShareModel } from './video-share' |
132 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | 119 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' |
133 | import { VideoTagModel } from './video-tag' | 120 | import { VideoTagModel } from './video-tag' |
134 | import { VideoViewModel } from './video-view' | 121 | import { VideoViewModel } from './video-view' |
135 | 122 | ||
136 | export enum ScopeNames { | 123 | export enum ScopeNames { |
137 | AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', | ||
138 | FOR_API = 'FOR_API', | 124 | FOR_API = 'FOR_API', |
139 | WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS', | 125 | WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS', |
140 | WITH_TAGS = 'WITH_TAGS', | 126 | WITH_TAGS = 'WITH_TAGS', |
141 | WITH_TRACKERS = 'WITH_TRACKERS', | ||
142 | WITH_WEBTORRENT_FILES = 'WITH_WEBTORRENT_FILES', | 127 | WITH_WEBTORRENT_FILES = 'WITH_WEBTORRENT_FILES', |
143 | WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', | 128 | WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', |
144 | WITH_BLACKLISTED = 'WITH_BLACKLISTED', | 129 | WITH_BLACKLISTED = 'WITH_BLACKLISTED', |
145 | WITH_USER_HISTORY = 'WITH_USER_HISTORY', | ||
146 | WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', | 130 | WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', |
147 | WITH_USER_ID = 'WITH_USER_ID', | ||
148 | WITH_IMMUTABLE_ATTRIBUTES = 'WITH_IMMUTABLE_ATTRIBUTES', | 131 | WITH_IMMUTABLE_ATTRIBUTES = 'WITH_IMMUTABLE_ATTRIBUTES', |
149 | WITH_THUMBNAILS = 'WITH_THUMBNAILS', | 132 | WITH_USER_HISTORY = 'WITH_USER_HISTORY', |
150 | WITH_LIVE = 'WITH_LIVE' | 133 | WITH_THUMBNAILS = 'WITH_THUMBNAILS' |
151 | } | 134 | } |
152 | 135 | ||
153 | export type ForAPIOptions = { | 136 | export type ForAPIOptions = { |
@@ -243,30 +226,6 @@ export type AvailableForListIDsOptions = { | |||
243 | } | 226 | } |
244 | ] | 227 | ] |
245 | }, | 228 | }, |
246 | [ScopeNames.WITH_LIVE]: { | ||
247 | include: [ | ||
248 | { | ||
249 | model: VideoLiveModel.unscoped(), | ||
250 | required: false | ||
251 | } | ||
252 | ] | ||
253 | }, | ||
254 | [ScopeNames.WITH_USER_ID]: { | ||
255 | include: [ | ||
256 | { | ||
257 | attributes: [ 'accountId' ], | ||
258 | model: VideoChannelModel.unscoped(), | ||
259 | required: true, | ||
260 | include: [ | ||
261 | { | ||
262 | attributes: [ 'userId' ], | ||
263 | model: AccountModel.unscoped(), | ||
264 | required: true | ||
265 | } | ||
266 | ] | ||
267 | } | ||
268 | ] | ||
269 | }, | ||
270 | [ScopeNames.WITH_ACCOUNT_DETAILS]: { | 229 | [ScopeNames.WITH_ACCOUNT_DETAILS]: { |
271 | include: [ | 230 | include: [ |
272 | { | 231 | { |
@@ -324,14 +283,6 @@ export type AvailableForListIDsOptions = { | |||
324 | [ScopeNames.WITH_TAGS]: { | 283 | [ScopeNames.WITH_TAGS]: { |
325 | include: [ TagModel ] | 284 | include: [ TagModel ] |
326 | }, | 285 | }, |
327 | [ScopeNames.WITH_TRACKERS]: { | ||
328 | include: [ | ||
329 | { | ||
330 | attributes: [ 'id', 'url' ], | ||
331 | model: TrackerModel | ||
332 | } | ||
333 | ] | ||
334 | }, | ||
335 | [ScopeNames.WITH_BLACKLISTED]: { | 286 | [ScopeNames.WITH_BLACKLISTED]: { |
336 | include: [ | 287 | include: [ |
337 | { | 288 | { |
@@ -489,7 +440,7 @@ export type AvailableForListIDsOptions = { | |||
489 | } | 440 | } |
490 | ] | 441 | ] |
491 | }) | 442 | }) |
492 | export class VideoModel extends Model { | 443 | export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { |
493 | 444 | ||
494 | @AllowNull(false) | 445 | @AllowNull(false) |
495 | @Default(DataType.UUIDV4) | 446 | @Default(DataType.UUIDV4) |
@@ -504,19 +455,16 @@ export class VideoModel extends Model { | |||
504 | 455 | ||
505 | @AllowNull(true) | 456 | @AllowNull(true) |
506 | @Default(null) | 457 | @Default(null) |
507 | @Is('VideoCategory', value => throwIfNotValid(value, isVideoCategoryValid, 'category', true)) | ||
508 | @Column | 458 | @Column |
509 | category: number | 459 | category: number |
510 | 460 | ||
511 | @AllowNull(true) | 461 | @AllowNull(true) |
512 | @Default(null) | 462 | @Default(null) |
513 | @Is('VideoLicence', value => throwIfNotValid(value, isVideoLicenceValid, 'licence', true)) | ||
514 | @Column | 463 | @Column |
515 | licence: number | 464 | licence: number |
516 | 465 | ||
517 | @AllowNull(true) | 466 | @AllowNull(true) |
518 | @Default(null) | 467 | @Default(null) |
519 | @Is('VideoLanguage', value => throwIfNotValid(value, isVideoLanguageValid, 'language', true)) | ||
520 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.LANGUAGE.max)) | 468 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.LANGUAGE.max)) |
521 | language: string | 469 | language: string |
522 | 470 | ||
@@ -624,7 +572,7 @@ export class VideoModel extends Model { | |||
624 | foreignKey: { | 572 | foreignKey: { |
625 | allowNull: true | 573 | allowNull: true |
626 | }, | 574 | }, |
627 | hooks: true | 575 | onDelete: 'cascade' |
628 | }) | 576 | }) |
629 | VideoChannel: VideoChannelModel | 577 | VideoChannel: VideoChannelModel |
630 | 578 | ||
@@ -802,14 +750,14 @@ export class VideoModel extends Model { | |||
802 | } | 750 | } |
803 | 751 | ||
804 | @BeforeDestroy | 752 | @BeforeDestroy |
805 | static async removeFiles (instance: VideoModel) { | 753 | static async removeFiles (instance: VideoModel, options) { |
806 | const tasks: Promise<any>[] = [] | 754 | const tasks: Promise<any>[] = [] |
807 | 755 | ||
808 | logger.info('Removing files of video %s.', instance.url) | 756 | logger.info('Removing files of video %s.', instance.url) |
809 | 757 | ||
810 | if (instance.isOwned()) { | 758 | if (instance.isOwned()) { |
811 | if (!Array.isArray(instance.VideoFiles)) { | 759 | if (!Array.isArray(instance.VideoFiles)) { |
812 | instance.VideoFiles = await instance.$get('VideoFiles') | 760 | instance.VideoFiles = await instance.$get('VideoFiles', { transaction: options.transaction }) |
813 | } | 761 | } |
814 | 762 | ||
815 | // Remove physical files and torrents | 763 | // Remove physical files and torrents |
@@ -820,7 +768,7 @@ export class VideoModel extends Model { | |||
820 | 768 | ||
821 | // Remove playlists file | 769 | // Remove playlists file |
822 | if (!Array.isArray(instance.VideoStreamingPlaylists)) { | 770 | if (!Array.isArray(instance.VideoStreamingPlaylists)) { |
823 | instance.VideoStreamingPlaylists = await instance.$get('VideoStreamingPlaylists') | 771 | instance.VideoStreamingPlaylists = await instance.$get('VideoStreamingPlaylists', { transaction: options.transaction }) |
824 | } | 772 | } |
825 | 773 | ||
826 | for (const p of instance.VideoStreamingPlaylists) { | 774 | for (const p of instance.VideoStreamingPlaylists) { |
@@ -843,7 +791,7 @@ export class VideoModel extends Model { | |||
843 | 791 | ||
844 | logger.info('Stopping live of video %s after video deletion.', instance.uuid) | 792 | logger.info('Stopping live of video %s after video deletion.', instance.uuid) |
845 | 793 | ||
846 | return LiveManager.Instance.stopSessionOf(instance.id) | 794 | LiveManager.Instance.stopSessionOf(instance.id) |
847 | } | 795 | } |
848 | 796 | ||
849 | @BeforeDestroy | 797 | @BeforeDestroy |
@@ -856,7 +804,7 @@ export class VideoModel extends Model { | |||
856 | const tasks: Promise<any>[] = [] | 804 | const tasks: Promise<any>[] = [] |
857 | 805 | ||
858 | if (!Array.isArray(instance.VideoAbuses)) { | 806 | if (!Array.isArray(instance.VideoAbuses)) { |
859 | instance.VideoAbuses = await instance.$get('VideoAbuses') | 807 | instance.VideoAbuses = await instance.$get('VideoAbuses', { transaction: options.transaction }) |
860 | 808 | ||
861 | if (instance.VideoAbuses.length === 0) return undefined | 809 | if (instance.VideoAbuses.length === 0) return undefined |
862 | } | 810 | } |
@@ -871,12 +819,7 @@ export class VideoModel extends Model { | |||
871 | tasks.push(abuse.save({ transaction: options.transaction })) | 819 | tasks.push(abuse.save({ transaction: options.transaction })) |
872 | } | 820 | } |
873 | 821 | ||
874 | Promise.all(tasks) | 822 | await Promise.all(tasks) |
875 | .catch(err => { | ||
876 | logger.error('Some errors when saving details of video %s in its abuses before destroy hook.', instance.uuid, { err }) | ||
877 | }) | ||
878 | |||
879 | return undefined | ||
880 | } | 823 | } |
881 | 824 | ||
882 | static listLocal (): Promise<MVideo[]> { | 825 | static listLocal (): Promise<MVideo[]> { |
@@ -1003,9 +946,9 @@ export class VideoModel extends Model { | |||
1003 | }) | 946 | }) |
1004 | } | 947 | } |
1005 | 948 | ||
1006 | static async listPublishedLiveIds () { | 949 | static async listPublishedLiveUUIDs () { |
1007 | const options = { | 950 | const options = { |
1008 | attributes: [ 'id' ], | 951 | attributes: [ 'uuid' ], |
1009 | where: { | 952 | where: { |
1010 | isLive: true, | 953 | isLive: true, |
1011 | remote: false, | 954 | remote: false, |
@@ -1015,7 +958,7 @@ export class VideoModel extends Model { | |||
1015 | 958 | ||
1016 | const result = await VideoModel.findAll(options) | 959 | const result = await VideoModel.findAll(options) |
1017 | 960 | ||
1018 | return result.map(v => v.id) | 961 | return result.map(v => v.uuid) |
1019 | } | 962 | } |
1020 | 963 | ||
1021 | static listUserVideosForApi (options: { | 964 | static listUserVideosForApi (options: { |
@@ -1298,27 +1241,16 @@ export class VideoModel extends Model { | |||
1298 | return VideoModel.count(options) | 1241 | return VideoModel.count(options) |
1299 | } | 1242 | } |
1300 | 1243 | ||
1301 | static load (id: number | string, t?: Transaction): Promise<MVideoThumbnail> { | 1244 | static load (id: number | string, transaction?: Transaction): Promise<MVideoThumbnail> { |
1302 | const where = buildWhereIdOrUUID(id) | 1245 | const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize) |
1303 | const options = { | ||
1304 | where, | ||
1305 | transaction: t | ||
1306 | } | ||
1307 | 1246 | ||
1308 | return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options) | 1247 | return queryBuilder.queryVideo({ id, transaction, type: 'thumbnails' }) |
1309 | } | 1248 | } |
1310 | 1249 | ||
1311 | static loadWithBlacklist (id: number | string, t?: Transaction): Promise<MVideoThumbnailBlacklist> { | 1250 | static loadWithBlacklist (id: number | string, transaction?: Transaction): Promise<MVideoThumbnailBlacklist> { |
1312 | const where = buildWhereIdOrUUID(id) | 1251 | const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize) |
1313 | const options = { | ||
1314 | where, | ||
1315 | transaction: t | ||
1316 | } | ||
1317 | 1252 | ||
1318 | return VideoModel.scope([ | 1253 | return queryBuilder.queryVideo({ id, transaction, type: 'thumbnails-blacklist' }) |
1319 | ScopeNames.WITH_THUMBNAILS, | ||
1320 | ScopeNames.WITH_BLACKLISTED | ||
1321 | ]).findOne(options) | ||
1322 | } | 1254 | } |
1323 | 1255 | ||
1324 | static loadImmutableAttributes (id: number | string, t?: Transaction): Promise<MVideoImmutable> { | 1256 | static loadImmutableAttributes (id: number | string, t?: Transaction): Promise<MVideoImmutable> { |
@@ -1339,68 +1271,6 @@ export class VideoModel extends Model { | |||
1339 | }) | 1271 | }) |
1340 | } | 1272 | } |
1341 | 1273 | ||
1342 | static loadWithRights (id: number | string, t?: Transaction): Promise<MVideoWithRights> { | ||
1343 | const where = buildWhereIdOrUUID(id) | ||
1344 | const options = { | ||
1345 | where, | ||
1346 | transaction: t | ||
1347 | } | ||
1348 | |||
1349 | return VideoModel.scope([ | ||
1350 | ScopeNames.WITH_BLACKLISTED, | ||
1351 | ScopeNames.WITH_USER_ID | ||
1352 | ]).findOne(options) | ||
1353 | } | ||
1354 | |||
1355 | static loadOnlyId (id: number | string, t?: Transaction): Promise<MVideoIdThumbnail> { | ||
1356 | const where = buildWhereIdOrUUID(id) | ||
1357 | |||
1358 | const options = { | ||
1359 | attributes: [ 'id' ], | ||
1360 | where, | ||
1361 | transaction: t | ||
1362 | } | ||
1363 | |||
1364 | return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options) | ||
1365 | } | ||
1366 | |||
1367 | static loadWithFiles (id: number | string, t?: Transaction, logging?: boolean): Promise<MVideoWithAllFiles> { | ||
1368 | const where = buildWhereIdOrUUID(id) | ||
1369 | |||
1370 | const query = { | ||
1371 | where, | ||
1372 | transaction: t, | ||
1373 | logging | ||
1374 | } | ||
1375 | |||
1376 | return VideoModel.scope([ | ||
1377 | ScopeNames.WITH_WEBTORRENT_FILES, | ||
1378 | ScopeNames.WITH_STREAMING_PLAYLISTS, | ||
1379 | ScopeNames.WITH_THUMBNAILS | ||
1380 | ]).findOne(query) | ||
1381 | } | ||
1382 | |||
1383 | static loadByUUID (uuid: string): Promise<MVideoThumbnail> { | ||
1384 | const options = { | ||
1385 | where: { | ||
1386 | uuid | ||
1387 | } | ||
1388 | } | ||
1389 | |||
1390 | return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options) | ||
1391 | } | ||
1392 | |||
1393 | static loadByUrl (url: string, transaction?: Transaction): Promise<MVideoThumbnail> { | ||
1394 | const query: FindOptions = { | ||
1395 | where: { | ||
1396 | url | ||
1397 | }, | ||
1398 | transaction | ||
1399 | } | ||
1400 | |||
1401 | return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query) | ||
1402 | } | ||
1403 | |||
1404 | static loadByUrlImmutableAttributes (url: string, transaction?: Transaction): Promise<MVideoImmutable> { | 1274 | static loadByUrlImmutableAttributes (url: string, transaction?: Transaction): Promise<MVideoImmutable> { |
1405 | const fun = () => { | 1275 | const fun = () => { |
1406 | const query: FindOptions = { | 1276 | const query: FindOptions = { |
@@ -1421,85 +1291,45 @@ export class VideoModel extends Model { | |||
1421 | }) | 1291 | }) |
1422 | } | 1292 | } |
1423 | 1293 | ||
1424 | static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Promise<MVideoAccountLightBlacklistAllFiles> { | 1294 | static loadOnlyId (id: number | string, transaction?: Transaction): Promise<MVideoId> { |
1425 | const query: FindOptions = { | 1295 | const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize) |
1426 | where: { | ||
1427 | url | ||
1428 | }, | ||
1429 | transaction | ||
1430 | } | ||
1431 | 1296 | ||
1432 | return VideoModel.scope([ | 1297 | return queryBuilder.queryVideo({ id, transaction, type: 'id' }) |
1433 | ScopeNames.WITH_ACCOUNT_DETAILS, | ||
1434 | ScopeNames.WITH_WEBTORRENT_FILES, | ||
1435 | ScopeNames.WITH_STREAMING_PLAYLISTS, | ||
1436 | ScopeNames.WITH_THUMBNAILS, | ||
1437 | ScopeNames.WITH_BLACKLISTED | ||
1438 | ]).findOne(query) | ||
1439 | } | 1298 | } |
1440 | 1299 | ||
1441 | static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Transaction, userId?: number): Promise<MVideoFullLight> { | 1300 | static loadWithFiles (id: number | string, transaction?: Transaction, logging?: boolean): Promise<MVideoWithAllFiles> { |
1442 | const where = buildWhereIdOrUUID(id) | 1301 | const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize) |
1443 | 1302 | ||
1444 | const options = { | 1303 | return queryBuilder.queryVideo({ id, transaction, type: 'all-files', logging }) |
1445 | order: [ [ 'Tags', 'name', 'ASC' ] ] as any, | 1304 | } |
1446 | where, | ||
1447 | transaction: t | ||
1448 | } | ||
1449 | 1305 | ||
1450 | const scopes: (string | ScopeOptions)[] = [ | 1306 | static loadByUrl (url: string, transaction?: Transaction): Promise<MVideoThumbnail> { |
1451 | ScopeNames.WITH_TAGS, | 1307 | const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize) |
1452 | ScopeNames.WITH_BLACKLISTED, | ||
1453 | ScopeNames.WITH_ACCOUNT_DETAILS, | ||
1454 | ScopeNames.WITH_SCHEDULED_UPDATE, | ||
1455 | ScopeNames.WITH_WEBTORRENT_FILES, | ||
1456 | ScopeNames.WITH_STREAMING_PLAYLISTS, | ||
1457 | ScopeNames.WITH_THUMBNAILS, | ||
1458 | ScopeNames.WITH_LIVE | ||
1459 | ] | ||
1460 | 1308 | ||
1461 | if (userId) { | 1309 | return queryBuilder.queryVideo({ url, transaction, type: 'thumbnails' }) |
1462 | scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] }) | 1310 | } |
1463 | } | 1311 | |
1312 | static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Promise<MVideoAccountLightBlacklistAllFiles> { | ||
1313 | const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize) | ||
1314 | |||
1315 | return queryBuilder.queryVideo({ url, transaction, type: 'account-blacklist-files' }) | ||
1316 | } | ||
1464 | 1317 | ||
1465 | return VideoModel | 1318 | static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Transaction, userId?: number): Promise<MVideoFullLight> { |
1466 | .scope(scopes) | 1319 | const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize) |
1467 | .findOne(options) | 1320 | |
1321 | return queryBuilder.queryVideo({ id, transaction: t, type: 'full-light', userId }) | ||
1468 | } | 1322 | } |
1469 | 1323 | ||
1470 | static loadForGetAPI (parameters: { | 1324 | static loadForGetAPI (parameters: { |
1471 | id: number | string | 1325 | id: number | string |
1472 | t?: Transaction | 1326 | transaction?: Transaction |
1473 | userId?: number | 1327 | userId?: number |
1474 | }): Promise<MVideoDetails> { | 1328 | }): Promise<MVideoDetails> { |
1475 | const { id, t, userId } = parameters | 1329 | const { id, transaction, userId } = parameters |
1476 | const where = buildWhereIdOrUUID(id) | 1330 | const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize) |
1477 | 1331 | ||
1478 | const options = { | 1332 | return queryBuilder.queryVideo({ id, transaction, type: 'api', userId }) |
1479 | order: [ [ 'Tags', 'name', 'ASC' ] ] as any, // FIXME: sequelize typings | ||
1480 | where, | ||
1481 | transaction: t | ||
1482 | } | ||
1483 | |||
1484 | const scopes: (string | ScopeOptions)[] = [ | ||
1485 | ScopeNames.WITH_TAGS, | ||
1486 | ScopeNames.WITH_BLACKLISTED, | ||
1487 | ScopeNames.WITH_ACCOUNT_DETAILS, | ||
1488 | ScopeNames.WITH_SCHEDULED_UPDATE, | ||
1489 | ScopeNames.WITH_THUMBNAILS, | ||
1490 | ScopeNames.WITH_LIVE, | ||
1491 | ScopeNames.WITH_TRACKERS, | ||
1492 | { method: [ ScopeNames.WITH_WEBTORRENT_FILES, true ] }, | ||
1493 | { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] } | ||
1494 | ] | ||
1495 | |||
1496 | if (userId) { | ||
1497 | scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] }) | ||
1498 | } | ||
1499 | |||
1500 | return VideoModel | ||
1501 | .scope(scopes) | ||
1502 | .findOne(options) | ||
1503 | } | 1333 | } |
1504 | 1334 | ||
1505 | static async getStats () { | 1335 | static async getStats () { |
@@ -1550,7 +1380,7 @@ export class VideoModel extends Model { | |||
1550 | 1380 | ||
1551 | const rawQuery = `UPDATE "video" SET "${field}" = ` + | 1381 | const rawQuery = `UPDATE "video" SET "${field}" = ` + |
1552 | '(' + | 1382 | '(' + |
1553 | 'SELECT COUNT(id) FROM "accountVideoRate" WHERE "accountVideoRate"."videoId" = "video"."id" AND type = :rateType' + | 1383 | 'SELECT COUNT(id) FROM "accountVideoRate" WHERE "accountVideoRate"."videoId" = "video"."id" AND type = :rateType' + |
1554 | ') ' + | 1384 | ') ' + |
1555 | 'WHERE "video"."id" = :videoId' | 1385 | 'WHERE "video"."id" = :videoId' |
1556 | 1386 | ||
@@ -1578,15 +1408,15 @@ export class VideoModel extends Model { | |||
1578 | .then(results => results.length === 1) | 1408 | .then(results => results.length === 1) |
1579 | } | 1409 | } |
1580 | 1410 | ||
1581 | static bulkUpdateSupportField (videoChannel: MChannel, t: Transaction) { | 1411 | static bulkUpdateSupportField (ofChannel: MChannel, t: Transaction) { |
1582 | const options = { | 1412 | const options = { |
1583 | where: { | 1413 | where: { |
1584 | channelId: videoChannel.id | 1414 | channelId: ofChannel.id |
1585 | }, | 1415 | }, |
1586 | transaction: t | 1416 | transaction: t |
1587 | } | 1417 | } |
1588 | 1418 | ||
1589 | return VideoModel.update({ support: videoChannel.support }, options) | 1419 | return VideoModel.update({ support: ofChannel.support }, options) |
1590 | } | 1420 | } |
1591 | 1421 | ||
1592 | static getAllIdsFromChannel (videoChannel: MChannelId): Promise<number[]> { | 1422 | static getAllIdsFromChannel (videoChannel: MChannelId): Promise<number[]> { |
@@ -1606,7 +1436,7 @@ export class VideoModel extends Model { | |||
1606 | const serverActor = await getServerActor() | 1436 | const serverActor = await getServerActor() |
1607 | const followerActorId = serverActor.id | 1437 | const followerActorId = serverActor.id |
1608 | 1438 | ||
1609 | const queryOptions: BuildVideosQueryOptions = { | 1439 | const queryOptions: BuildVideosListQueryOptions = { |
1610 | attributes: [ `"${field}"` ], | 1440 | attributes: [ `"${field}"` ], |
1611 | group: `GROUP BY "${field}"`, | 1441 | group: `GROUP BY "${field}"`, |
1612 | having: `HAVING COUNT("${field}") >= ${threshold}`, | 1442 | having: `HAVING COUNT("${field}") >= ${threshold}`, |
@@ -1618,10 +1448,10 @@ export class VideoModel extends Model { | |||
1618 | includeLocalVideos: true | 1448 | includeLocalVideos: true |
1619 | } | 1449 | } |
1620 | 1450 | ||
1621 | const { query, replacements } = buildListQuery(VideoModel, queryOptions) | 1451 | const queryBuilder = new VideosIdListQueryBuilder(VideoModel.sequelize) |
1622 | 1452 | ||
1623 | return this.sequelize.query<any>(query, { replacements, type: QueryTypes.SELECT }) | 1453 | return queryBuilder.queryVideoIds(queryOptions) |
1624 | .then(rows => rows.map(r => r[field])) | 1454 | .then(rows => rows.map(r => r[field])) |
1625 | } | 1455 | } |
1626 | 1456 | ||
1627 | static buildTrendingQuery (trendingDays: number) { | 1457 | static buildTrendingQuery (trendingDays: number) { |
@@ -1639,27 +1469,24 @@ export class VideoModel extends Model { | |||
1639 | } | 1469 | } |
1640 | 1470 | ||
1641 | private static async getAvailableForApi ( | 1471 | private static async getAvailableForApi ( |
1642 | options: BuildVideosQueryOptions, | 1472 | options: BuildVideosListQueryOptions, |
1643 | countVideos = true | 1473 | countVideos = true |
1644 | ): Promise<ResultList<VideoModel>> { | 1474 | ): Promise<ResultList<VideoModel>> { |
1645 | function getCount () { | 1475 | function getCount () { |
1646 | if (countVideos !== true) return Promise.resolve(undefined) | 1476 | if (countVideos !== true) return Promise.resolve(undefined) |
1647 | 1477 | ||
1648 | const countOptions = Object.assign({}, options, { isCount: true }) | 1478 | const countOptions = Object.assign({}, options, { isCount: true }) |
1649 | const { query: queryCount, replacements: replacementsCount } = buildListQuery(VideoModel, countOptions) | 1479 | const queryBuilder = new VideosIdListQueryBuilder(VideoModel.sequelize) |
1650 | 1480 | ||
1651 | return VideoModel.sequelize.query<any>(queryCount, { replacements: replacementsCount, type: QueryTypes.SELECT }) | 1481 | return queryBuilder.countVideoIds(countOptions) |
1652 | .then(rows => rows.length !== 0 ? rows[0].total : 0) | ||
1653 | } | 1482 | } |
1654 | 1483 | ||
1655 | function getModels () { | 1484 | function getModels () { |
1656 | if (options.count === 0) return Promise.resolve([]) | 1485 | if (options.count === 0) return Promise.resolve([]) |
1657 | 1486 | ||
1658 | const { query, replacements, order } = buildListQuery(VideoModel, options) | 1487 | const queryBuilder = new VideosModelListQueryBuilder(VideoModel.sequelize) |
1659 | const queryModels = wrapForAPIResults(query, replacements, options, order) | ||
1660 | 1488 | ||
1661 | return VideoModel.sequelize.query<any>(queryModels, { replacements, type: QueryTypes.SELECT, nest: true }) | 1489 | return queryBuilder.queryVideos(options) |
1662 | .then(rows => VideoModel.buildAPIResult(rows)) | ||
1663 | } | 1490 | } |
1664 | 1491 | ||
1665 | const [ count, rows ] = await Promise.all([ getCount(), getModels() ]) | 1492 | const [ count, rows ] = await Promise.all([ getCount(), getModels() ]) |
@@ -1670,173 +1497,6 @@ export class VideoModel extends Model { | |||
1670 | } | 1497 | } |
1671 | } | 1498 | } |
1672 | 1499 | ||
1673 | private static buildAPIResult (rows: any[]) { | ||
1674 | const videosMemo: { [ id: number ]: VideoModel } = {} | ||
1675 | const videoStreamingPlaylistMemo: { [ id: number ]: VideoStreamingPlaylistModel } = {} | ||
1676 | |||
1677 | const thumbnailsDone = new Set<number>() | ||
1678 | const historyDone = new Set<number>() | ||
1679 | const videoFilesDone = new Set<number>() | ||
1680 | |||
1681 | const videos: VideoModel[] = [] | ||
1682 | |||
1683 | const avatarKeys = [ 'id', 'filename', 'fileUrl', 'onDisk', 'createdAt', 'updatedAt' ] | ||
1684 | const actorKeys = [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ] | ||
1685 | const serverKeys = [ 'id', 'host' ] | ||
1686 | const videoFileKeys = [ | ||
1687 | 'id', | ||
1688 | 'createdAt', | ||
1689 | 'updatedAt', | ||
1690 | 'resolution', | ||
1691 | 'size', | ||
1692 | 'extname', | ||
1693 | 'filename', | ||
1694 | 'fileUrl', | ||
1695 | 'torrentFilename', | ||
1696 | 'torrentUrl', | ||
1697 | 'infoHash', | ||
1698 | 'fps', | ||
1699 | 'videoId', | ||
1700 | 'videoStreamingPlaylistId' | ||
1701 | ] | ||
1702 | const videoStreamingPlaylistKeys = [ 'id', 'type', 'playlistUrl' ] | ||
1703 | const videoKeys = [ | ||
1704 | 'id', | ||
1705 | 'uuid', | ||
1706 | 'name', | ||
1707 | 'category', | ||
1708 | 'licence', | ||
1709 | 'language', | ||
1710 | 'privacy', | ||
1711 | 'nsfw', | ||
1712 | 'description', | ||
1713 | 'support', | ||
1714 | 'duration', | ||
1715 | 'views', | ||
1716 | 'likes', | ||
1717 | 'dislikes', | ||
1718 | 'remote', | ||
1719 | 'isLive', | ||
1720 | 'url', | ||
1721 | 'commentsEnabled', | ||
1722 | 'downloadEnabled', | ||
1723 | 'waitTranscoding', | ||
1724 | 'state', | ||
1725 | 'publishedAt', | ||
1726 | 'originallyPublishedAt', | ||
1727 | 'channelId', | ||
1728 | 'createdAt', | ||
1729 | 'updatedAt' | ||
1730 | ] | ||
1731 | const buildOpts = { raw: true } | ||
1732 | |||
1733 | function buildActor (rowActor: any) { | ||
1734 | const avatarModel = rowActor.Avatar.id !== null | ||
1735 | ? new ActorImageModel(pick(rowActor.Avatar, avatarKeys), buildOpts) | ||
1736 | : null | ||
1737 | |||
1738 | const serverModel = rowActor.Server.id !== null | ||
1739 | ? new ServerModel(pick(rowActor.Server, serverKeys), buildOpts) | ||
1740 | : null | ||
1741 | |||
1742 | const actorModel = new ActorModel(pick(rowActor, actorKeys), buildOpts) | ||
1743 | actorModel.Avatar = avatarModel | ||
1744 | actorModel.Server = serverModel | ||
1745 | |||
1746 | return actorModel | ||
1747 | } | ||
1748 | |||
1749 | for (const row of rows) { | ||
1750 | if (!videosMemo[row.id]) { | ||
1751 | // Build Channel | ||
1752 | const channel = row.VideoChannel | ||
1753 | const channelModel = new VideoChannelModel(pick(channel, [ 'id', 'name', 'description', 'actorId' ]), buildOpts) | ||
1754 | channelModel.Actor = buildActor(channel.Actor) | ||
1755 | |||
1756 | const account = row.VideoChannel.Account | ||
1757 | const accountModel = new AccountModel(pick(account, [ 'id', 'name' ]), buildOpts) | ||
1758 | accountModel.Actor = buildActor(account.Actor) | ||
1759 | |||
1760 | channelModel.Account = accountModel | ||
1761 | |||
1762 | const videoModel = new VideoModel(pick(row, videoKeys), buildOpts) | ||
1763 | videoModel.VideoChannel = channelModel | ||
1764 | |||
1765 | videoModel.UserVideoHistories = [] | ||
1766 | videoModel.Thumbnails = [] | ||
1767 | videoModel.VideoFiles = [] | ||
1768 | videoModel.VideoStreamingPlaylists = [] | ||
1769 | |||
1770 | videosMemo[row.id] = videoModel | ||
1771 | // Don't take object value to have a sorted array | ||
1772 | videos.push(videoModel) | ||
1773 | } | ||
1774 | |||
1775 | const videoModel = videosMemo[row.id] | ||
1776 | |||
1777 | if (row.userVideoHistory?.id && !historyDone.has(row.userVideoHistory.id)) { | ||
1778 | const historyModel = new UserVideoHistoryModel(pick(row.userVideoHistory, [ 'id', 'currentTime' ]), buildOpts) | ||
1779 | videoModel.UserVideoHistories.push(historyModel) | ||
1780 | |||
1781 | historyDone.add(row.userVideoHistory.id) | ||
1782 | } | ||
1783 | |||
1784 | if (row.Thumbnails?.id && !thumbnailsDone.has(row.Thumbnails.id)) { | ||
1785 | const thumbnailModel = new ThumbnailModel(pick(row.Thumbnails, [ 'id', 'type', 'filename' ]), buildOpts) | ||
1786 | videoModel.Thumbnails.push(thumbnailModel) | ||
1787 | |||
1788 | thumbnailsDone.add(row.Thumbnails.id) | ||
1789 | } | ||
1790 | |||
1791 | if (row.VideoFiles?.id && !videoFilesDone.has(row.VideoFiles.id)) { | ||
1792 | const videoFileModel = new VideoFileModel(pick(row.VideoFiles, videoFileKeys), buildOpts) | ||
1793 | videoModel.VideoFiles.push(videoFileModel) | ||
1794 | |||
1795 | videoFilesDone.add(row.VideoFiles.id) | ||
1796 | } | ||
1797 | |||
1798 | if (row.VideoStreamingPlaylists?.id && !videoStreamingPlaylistMemo[row.VideoStreamingPlaylists.id]) { | ||
1799 | const streamingPlaylist = new VideoStreamingPlaylistModel(pick(row.VideoStreamingPlaylists, videoStreamingPlaylistKeys), buildOpts) | ||
1800 | streamingPlaylist.VideoFiles = [] | ||
1801 | |||
1802 | videoModel.VideoStreamingPlaylists.push(streamingPlaylist) | ||
1803 | |||
1804 | videoStreamingPlaylistMemo[streamingPlaylist.id] = streamingPlaylist | ||
1805 | } | ||
1806 | |||
1807 | if (row.VideoStreamingPlaylists?.VideoFiles?.id && !videoFilesDone.has(row.VideoStreamingPlaylists.VideoFiles.id)) { | ||
1808 | const streamingPlaylist = videoStreamingPlaylistMemo[row.VideoStreamingPlaylists.id] | ||
1809 | |||
1810 | const videoFileModel = new VideoFileModel(pick(row.VideoStreamingPlaylists.VideoFiles, videoFileKeys), buildOpts) | ||
1811 | streamingPlaylist.VideoFiles.push(videoFileModel) | ||
1812 | |||
1813 | videoFilesDone.add(row.VideoStreamingPlaylists.VideoFiles.id) | ||
1814 | } | ||
1815 | } | ||
1816 | |||
1817 | return videos | ||
1818 | } | ||
1819 | |||
1820 | static getCategoryLabel (id: number) { | ||
1821 | return VIDEO_CATEGORIES[id] || 'Misc' | ||
1822 | } | ||
1823 | |||
1824 | static getLicenceLabel (id: number) { | ||
1825 | return VIDEO_LICENCES[id] || 'Unknown' | ||
1826 | } | ||
1827 | |||
1828 | static getLanguageLabel (id: string) { | ||
1829 | return VIDEO_LANGUAGES[id] || 'Unknown' | ||
1830 | } | ||
1831 | |||
1832 | static getPrivacyLabel (id: number) { | ||
1833 | return VIDEO_PRIVACIES[id] || 'Unknown' | ||
1834 | } | ||
1835 | |||
1836 | static getStateLabel (id: number) { | ||
1837 | return VIDEO_STATES[id] || 'Unknown' | ||
1838 | } | ||
1839 | |||
1840 | isBlacklisted () { | 1500 | isBlacklisted () { |
1841 | return !!this.VideoBlacklist | 1501 | return !!this.VideoBlacklist |
1842 | } | 1502 | } |
@@ -1885,7 +1545,7 @@ export class VideoModel extends Model { | |||
1885 | return Array.isArray(this.VideoFiles) === true && this.VideoFiles.length !== 0 | 1545 | return Array.isArray(this.VideoFiles) === true && this.VideoFiles.length !== 0 |
1886 | } | 1546 | } |
1887 | 1547 | ||
1888 | async addAndSaveThumbnail (thumbnail: MThumbnail, transaction: Transaction) { | 1548 | async addAndSaveThumbnail (thumbnail: MThumbnail, transaction?: Transaction) { |
1889 | thumbnail.videoId = this.id | 1549 | thumbnail.videoId = this.id |
1890 | 1550 | ||
1891 | const savedThumbnail = await thumbnail.save({ transaction }) | 1551 | const savedThumbnail = await thumbnail.save({ transaction }) |
@@ -1919,7 +1579,7 @@ export class VideoModel extends Model { | |||
1919 | } | 1579 | } |
1920 | 1580 | ||
1921 | getWatchStaticPath () { | 1581 | getWatchStaticPath () { |
1922 | return '/videos/watch/' + this.uuid | 1582 | return '/w/' + this.uuid |
1923 | } | 1583 | } |
1924 | 1584 | ||
1925 | getEmbedStaticPath () { | 1585 | getEmbedStaticPath () { |