aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/video
diff options
context:
space:
mode:
Diffstat (limited to 'server/models/video')
-rw-r--r--server/models/video/formatter/video-format-utils.ts2
-rw-r--r--server/models/video/sql/comment/video-comment-list-query-builder.ts400
-rw-r--r--server/models/video/sql/comment/video-comment-table-attributes.ts43
-rw-r--r--server/models/video/sql/video/shared/abstract-video-query-builder.ts2
-rw-r--r--server/models/video/sql/video/videos-id-list-query-builder.ts7
-rw-r--r--server/models/video/tag.ts2
-rw-r--r--server/models/video/video-blacklist.ts6
-rw-r--r--server/models/video/video-caption.ts2
-rw-r--r--server/models/video/video-change-ownership.ts2
-rw-r--r--server/models/video/video-channel-sync.ts2
-rw-r--r--server/models/video/video-channel.ts12
-rw-r--r--server/models/video/video-comment.ts458
-rw-r--r--server/models/video/video-file.ts13
-rw-r--r--server/models/video/video-import.ts2
-rw-r--r--server/models/video/video-playlist-element.ts41
-rw-r--r--server/models/video/video-playlist.ts12
-rw-r--r--server/models/video/video-share.ts2
-rw-r--r--server/models/video/video-streaming-playlist.ts7
-rw-r--r--server/models/video/video.ts7
19 files changed, 610 insertions, 412 deletions
diff --git a/server/models/video/formatter/video-format-utils.ts b/server/models/video/formatter/video-format-utils.ts
index f285db477..6f05dbdc8 100644
--- a/server/models/video/formatter/video-format-utils.ts
+++ b/server/models/video/formatter/video-format-utils.ts
@@ -488,7 +488,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
488} 488}
489 489
490function getCategoryLabel (id: number) { 490function getCategoryLabel (id: number) {
491 return VIDEO_CATEGORIES[id] || 'Misc' 491 return VIDEO_CATEGORIES[id] || 'Unknown'
492} 492}
493 493
494function getLicenceLabel (id: number) { 494function getLicenceLabel (id: number) {
diff --git a/server/models/video/sql/comment/video-comment-list-query-builder.ts b/server/models/video/sql/comment/video-comment-list-query-builder.ts
new file mode 100644
index 000000000..a7eed22a1
--- /dev/null
+++ b/server/models/video/sql/comment/video-comment-list-query-builder.ts
@@ -0,0 +1,400 @@
1import { Model, Sequelize, Transaction } from 'sequelize'
2import { AbstractRunQuery, ModelBuilder } from '@server/models/shared'
3import { ActorImageType, VideoPrivacy } from '@shared/models'
4import { createSafeIn, getSort, parseRowCountResult } from '../../../shared'
5import { VideoCommentTableAttributes } from './video-comment-table-attributes'
6
7export interface ListVideoCommentsOptions {
8 selectType: 'api' | 'feed' | 'comment-only'
9
10 start?: number
11 count?: number
12 sort?: string
13
14 videoId?: number
15 threadId?: number
16 accountId?: number
17 videoChannelId?: number
18
19 blockerAccountIds?: number[]
20
21 isThread?: boolean
22 notDeleted?: boolean
23 isLocal?: boolean
24 onLocalVideo?: boolean
25 onPublicVideo?: boolean
26 videoAccountOwnerId?: boolean
27
28 search?: string
29 searchAccount?: string
30 searchVideo?: string
31
32 includeReplyCounters?: boolean
33
34 transaction?: Transaction
35}
36
37export class VideoCommentListQueryBuilder extends AbstractRunQuery {
38 private readonly tableAttributes = new VideoCommentTableAttributes()
39
40 private innerQuery: string
41
42 private select = ''
43 private joins = ''
44
45 private innerSelect = ''
46 private innerJoins = ''
47 private innerLateralJoins = ''
48 private innerWhere = ''
49
50 private readonly built = {
51 cte: false,
52 accountJoin: false,
53 videoJoin: false,
54 videoChannelJoin: false,
55 avatarJoin: false
56 }
57
58 constructor (
59 protected readonly sequelize: Sequelize,
60 private readonly options: ListVideoCommentsOptions
61 ) {
62 super(sequelize)
63
64 if (this.options.includeReplyCounters && !this.options.videoId) {
65 throw new Error('Cannot include reply counters without videoId')
66 }
67 }
68
69 async listComments <T extends Model> () {
70 this.buildListQuery()
71
72 const results = await this.runQuery({ nest: true, transaction: this.options.transaction })
73 const modelBuilder = new ModelBuilder<T>(this.sequelize)
74
75 return modelBuilder.createModels(results, 'VideoComment')
76 }
77
78 async countComments () {
79 this.buildCountQuery()
80
81 const result = await this.runQuery({ transaction: this.options.transaction })
82
83 return parseRowCountResult(result)
84 }
85
86 // ---------------------------------------------------------------------------
87
88 private buildListQuery () {
89 this.buildInnerListQuery()
90 this.buildListSelect()
91
92 this.query = `${this.select} ` +
93 `FROM (${this.innerQuery}) AS "VideoCommentModel" ` +
94 `${this.joins} ` +
95 `${this.getOrder()}`
96 }
97
98 private buildInnerListQuery () {
99 this.buildWhere()
100 this.buildInnerListSelect()
101
102 this.innerQuery = `${this.innerSelect} ` +
103 `FROM "videoComment" AS "VideoCommentModel" ` +
104 `${this.innerJoins} ` +
105 `${this.innerLateralJoins} ` +
106 `${this.innerWhere} ` +
107 `${this.getOrder()} ` +
108 `${this.getInnerLimit()}`
109 }
110
111 // ---------------------------------------------------------------------------
112
113 private buildCountQuery () {
114 this.buildWhere()
115
116 this.query = `SELECT COUNT(*) AS "total" ` +
117 `FROM "videoComment" AS "VideoCommentModel" ` +
118 `${this.innerJoins} ` +
119 `${this.innerWhere}`
120 }
121
122 // ---------------------------------------------------------------------------
123
124 private buildWhere () {
125 let where: string[] = []
126
127 if (this.options.videoId) {
128 this.replacements.videoId = this.options.videoId
129
130 where.push('"VideoCommentModel"."videoId" = :videoId')
131 }
132
133 if (this.options.threadId) {
134 this.replacements.threadId = this.options.threadId
135
136 where.push('("VideoCommentModel"."id" = :threadId OR "VideoCommentModel"."originCommentId" = :threadId)')
137 }
138
139 if (this.options.accountId) {
140 this.replacements.accountId = this.options.accountId
141
142 where.push('"VideoCommentModel"."accountId" = :accountId')
143 }
144
145 if (this.options.videoChannelId) {
146 this.buildVideoChannelJoin()
147
148 this.replacements.videoChannelId = this.options.videoChannelId
149
150 where.push('"Account->VideoChannel"."id" = :videoChannelId')
151 }
152
153 if (this.options.blockerAccountIds) {
154 this.buildVideoChannelJoin()
155
156 where = where.concat(this.getBlockWhere('VideoCommentModel', 'Video->VideoChannel'))
157 }
158
159 if (this.options.isThread === true) {
160 where.push('"VideoCommentModel"."inReplyToCommentId" IS NULL')
161 }
162
163 if (this.options.notDeleted === true) {
164 where.push('"VideoCommentModel"."deletedAt" IS NULL')
165 }
166
167 if (this.options.isLocal === true) {
168 this.buildAccountJoin()
169
170 where.push('"Account->Actor"."serverId" IS NULL')
171 } else if (this.options.isLocal === false) {
172 this.buildAccountJoin()
173
174 where.push('"Account->Actor"."serverId" IS NOT NULL')
175 }
176
177 if (this.options.onLocalVideo === true) {
178 this.buildVideoJoin()
179
180 where.push('"Video"."remote" IS FALSE')
181 } else if (this.options.onLocalVideo === false) {
182 this.buildVideoJoin()
183
184 where.push('"Video"."remote" IS TRUE')
185 }
186
187 if (this.options.onPublicVideo === true) {
188 this.buildVideoJoin()
189
190 where.push(`"Video"."privacy" = ${VideoPrivacy.PUBLIC}`)
191 }
192
193 if (this.options.videoAccountOwnerId) {
194 this.buildVideoChannelJoin()
195
196 this.replacements.videoAccountOwnerId = this.options.videoAccountOwnerId
197
198 where.push(`"Video->VideoChannel"."accountId" = :videoAccountOwnerId`)
199 }
200
201 if (this.options.search) {
202 this.buildVideoJoin()
203 this.buildAccountJoin()
204
205 const escapedLikeSearch = this.sequelize.escape('%' + this.options.search + '%')
206
207 where.push(
208 `(` +
209 `"VideoCommentModel"."text" ILIKE ${escapedLikeSearch} OR ` +
210 `"Account->Actor"."preferredUsername" ILIKE ${escapedLikeSearch} OR ` +
211 `"Account"."name" ILIKE ${escapedLikeSearch} OR ` +
212 `"Video"."name" ILIKE ${escapedLikeSearch} ` +
213 `)`
214 )
215 }
216
217 if (this.options.searchAccount) {
218 this.buildAccountJoin()
219
220 const escapedLikeSearch = this.sequelize.escape('%' + this.options.searchAccount + '%')
221
222 where.push(
223 `(` +
224 `"Account->Actor"."preferredUsername" ILIKE ${escapedLikeSearch} OR ` +
225 `"Account"."name" ILIKE ${escapedLikeSearch} ` +
226 `)`
227 )
228 }
229
230 if (this.options.searchVideo) {
231 this.buildVideoJoin()
232
233 const escapedLikeSearch = this.sequelize.escape('%' + this.options.searchVideo + '%')
234
235 where.push(`"Video"."name" ILIKE ${escapedLikeSearch}`)
236 }
237
238 if (where.length !== 0) {
239 this.innerWhere = `WHERE ${where.join(' AND ')}`
240 }
241 }
242
243 private buildAccountJoin () {
244 if (this.built.accountJoin) return
245
246 this.innerJoins += ' LEFT JOIN "account" "Account" ON "Account"."id" = "VideoCommentModel"."accountId" ' +
247 'LEFT JOIN "actor" "Account->Actor" ON "Account->Actor"."id" = "Account"."actorId" ' +
248 'LEFT JOIN "server" "Account->Actor->Server" ON "Account->Actor"."serverId" = "Account->Actor->Server"."id" '
249
250 this.built.accountJoin = true
251 }
252
253 private buildVideoJoin () {
254 if (this.built.videoJoin) return
255
256 this.innerJoins += ' LEFT JOIN "video" "Video" ON "Video"."id" = "VideoCommentModel"."videoId" '
257
258 this.built.videoJoin = true
259 }
260
261 private buildVideoChannelJoin () {
262 if (this.built.videoChannelJoin) return
263
264 this.buildVideoJoin()
265
266 this.innerJoins += ' LEFT JOIN "videoChannel" "Video->VideoChannel" ON "Video"."channelId" = "Video->VideoChannel"."id" '
267
268 this.built.videoChannelJoin = true
269 }
270
271 private buildAvatarsJoin () {
272 if (this.options.selectType !== 'api' && this.options.selectType !== 'feed') return ''
273 if (this.built.avatarJoin) return
274
275 this.joins += `LEFT JOIN "actorImage" "Account->Actor->Avatars" ` +
276 `ON "VideoCommentModel"."Account.Actor.id" = "Account->Actor->Avatars"."actorId" ` +
277 `AND "Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR}`
278
279 this.built.avatarJoin = true
280 }
281
282 // ---------------------------------------------------------------------------
283
284 private buildListSelect () {
285 const toSelect = [ '"VideoCommentModel".*' ]
286
287 if (this.options.selectType === 'api' || this.options.selectType === 'feed') {
288 this.buildAvatarsJoin()
289
290 toSelect.push(this.tableAttributes.getAvatarAttributes())
291 }
292
293 this.select = this.buildSelect(toSelect)
294 }
295
296 private buildInnerListSelect () {
297 let toSelect = [ this.tableAttributes.getVideoCommentAttributes() ]
298
299 if (this.options.selectType === 'api' || this.options.selectType === 'feed') {
300 this.buildAccountJoin()
301 this.buildVideoJoin()
302
303 toSelect = toSelect.concat([
304 this.tableAttributes.getVideoAttributes(),
305 this.tableAttributes.getAccountAttributes(),
306 this.tableAttributes.getActorAttributes(),
307 this.tableAttributes.getServerAttributes()
308 ])
309 }
310
311 if (this.options.includeReplyCounters === true) {
312 this.buildTotalRepliesSelect()
313 this.buildAuthorTotalRepliesSelect()
314
315 toSelect.push('"totalRepliesFromVideoAuthor"."count" AS "totalRepliesFromVideoAuthor"')
316 toSelect.push('"totalReplies"."count" AS "totalReplies"')
317 }
318
319 this.innerSelect = this.buildSelect(toSelect)
320 }
321
322 // ---------------------------------------------------------------------------
323
324 private getBlockWhere (commentTableName: string, channelTableName: string) {
325 const where: string[] = []
326
327 const blockerIdsString = createSafeIn(
328 this.sequelize,
329 this.options.blockerAccountIds,
330 [ `"${channelTableName}"."accountId"` ]
331 )
332
333 where.push(
334 `NOT EXISTS (` +
335 `SELECT 1 FROM "accountBlocklist" ` +
336 `WHERE "targetAccountId" = "${commentTableName}"."accountId" ` +
337 `AND "accountId" IN (${blockerIdsString})` +
338 `)`
339 )
340
341 where.push(
342 `NOT EXISTS (` +
343 `SELECT 1 FROM "account" ` +
344 `INNER JOIN "actor" ON account."actorId" = actor.id ` +
345 `INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ` +
346 `WHERE "account"."id" = "${commentTableName}"."accountId" ` +
347 `AND "serverBlocklist"."accountId" IN (${blockerIdsString})` +
348 `)`
349 )
350
351 return where
352 }
353
354 // ---------------------------------------------------------------------------
355
356 private buildTotalRepliesSelect () {
357 const blockWhereString = this.getBlockWhere('replies', 'videoChannel').join(' AND ')
358
359 // Help the planner by providing videoId that should filter out many comments
360 this.replacements.videoId = this.options.videoId
361
362 this.innerLateralJoins += `LEFT JOIN LATERAL (` +
363 `SELECT COUNT("replies"."id") AS "count" FROM "videoComment" AS "replies" ` +
364 `INNER JOIN "video" ON "video"."id" = "replies"."videoId" AND "replies"."videoId" = :videoId ` +
365 `LEFT JOIN "videoChannel" ON "video"."channelId" = "videoChannel"."id" ` +
366 `WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ` +
367 `AND "deletedAt" IS NULL ` +
368 `AND ${blockWhereString} ` +
369 `) "totalReplies" ON TRUE `
370 }
371
372 private buildAuthorTotalRepliesSelect () {
373 // Help the planner by providing videoId that should filter out many comments
374 this.replacements.videoId = this.options.videoId
375
376 this.innerLateralJoins += `LEFT JOIN LATERAL (` +
377 `SELECT COUNT("replies"."id") AS "count" FROM "videoComment" AS "replies" ` +
378 `INNER JOIN "video" ON "video"."id" = "replies"."videoId" AND "replies"."videoId" = :videoId ` +
379 `INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ` +
380 `WHERE "replies"."originCommentId" = "VideoCommentModel"."id" AND "replies"."accountId" = "videoChannel"."accountId"` +
381 `) "totalRepliesFromVideoAuthor" ON TRUE `
382 }
383
384 private getOrder () {
385 if (!this.options.sort) return ''
386
387 const orders = getSort(this.options.sort)
388
389 return 'ORDER BY ' + orders.map(o => `"${o[0]}" ${o[1]}`).join(', ')
390 }
391
392 private getInnerLimit () {
393 if (!this.options.count) return ''
394
395 this.replacements.limit = this.options.count
396 this.replacements.offset = this.options.start || 0
397
398 return `LIMIT :limit OFFSET :offset `
399 }
400}
diff --git a/server/models/video/sql/comment/video-comment-table-attributes.ts b/server/models/video/sql/comment/video-comment-table-attributes.ts
new file mode 100644
index 000000000..87f8750c1
--- /dev/null
+++ b/server/models/video/sql/comment/video-comment-table-attributes.ts
@@ -0,0 +1,43 @@
1import { Memoize } from '@server/helpers/memoize'
2import { AccountModel } from '@server/models/account/account'
3import { ActorModel } from '@server/models/actor/actor'
4import { ActorImageModel } from '@server/models/actor/actor-image'
5import { ServerModel } from '@server/models/server/server'
6import { VideoCommentModel } from '../../video-comment'
7
8export class VideoCommentTableAttributes {
9
10 @Memoize()
11 getVideoCommentAttributes () {
12 return VideoCommentModel.getSQLAttributes('VideoCommentModel').join(', ')
13 }
14
15 @Memoize()
16 getAccountAttributes () {
17 return AccountModel.getSQLAttributes('Account', 'Account.').join(', ')
18 }
19
20 @Memoize()
21 getVideoAttributes () {
22 return [
23 `"Video"."id" AS "Video.id"`,
24 `"Video"."uuid" AS "Video.uuid"`,
25 `"Video"."name" AS "Video.name"`
26 ].join(', ')
27 }
28
29 @Memoize()
30 getActorAttributes () {
31 return ActorModel.getSQLAPIAttributes('Account->Actor', `Account.Actor.`).join(', ')
32 }
33
34 @Memoize()
35 getServerAttributes () {
36 return ServerModel.getSQLAttributes('Account->Actor->Server', `Account.Actor.Server.`).join(', ')
37 }
38
39 @Memoize()
40 getAvatarAttributes () {
41 return ActorImageModel.getSQLAttributes('Account->Actor->Avatars', 'Account.Actor.Avatars.').join(', ')
42 }
43}
diff --git a/server/models/video/sql/video/shared/abstract-video-query-builder.ts b/server/models/video/sql/video/shared/abstract-video-query-builder.ts
index f0ce69501..cbd57ad8c 100644
--- a/server/models/video/sql/video/shared/abstract-video-query-builder.ts
+++ b/server/models/video/sql/video/shared/abstract-video-query-builder.ts
@@ -1,9 +1,9 @@
1import { Sequelize } from 'sequelize' 1import { Sequelize } from 'sequelize'
2import validator from 'validator' 2import validator from 'validator'
3import { createSafeIn } from '@server/models/utils'
4import { MUserAccountId } from '@server/types/models' 3import { MUserAccountId } from '@server/types/models'
5import { ActorImageType } from '@shared/models' 4import { ActorImageType } from '@shared/models'
6import { AbstractRunQuery } from '../../../../shared/abstract-run-query' 5import { AbstractRunQuery } from '../../../../shared/abstract-run-query'
6import { createSafeIn } from '../../../../shared'
7import { VideoTableAttributes } from './video-table-attributes' 7import { VideoTableAttributes } from './video-table-attributes'
8 8
9/** 9/**
diff --git a/server/models/video/sql/video/videos-id-list-query-builder.ts b/server/models/video/sql/video/videos-id-list-query-builder.ts
index 7c864bf27..62f1855c7 100644
--- a/server/models/video/sql/video/videos-id-list-query-builder.ts
+++ b/server/models/video/sql/video/videos-id-list-query-builder.ts
@@ -2,11 +2,12 @@ import { Sequelize, Transaction } from 'sequelize'
2import validator from 'validator' 2import validator from 'validator'
3import { exists } from '@server/helpers/custom-validators/misc' 3import { exists } from '@server/helpers/custom-validators/misc'
4import { WEBSERVER } from '@server/initializers/constants' 4import { WEBSERVER } from '@server/initializers/constants'
5import { buildDirectionAndField, createSafeIn, parseRowCountResult } from '@server/models/utils' 5import { buildSortDirectionAndField } from '@server/models/shared'
6import { MUserAccountId, MUserId } from '@server/types/models' 6import { MUserAccountId, MUserId } from '@server/types/models'
7import { forceNumber } from '@shared/core-utils'
7import { VideoInclude, VideoPrivacy, VideoState } from '@shared/models' 8import { VideoInclude, VideoPrivacy, VideoState } from '@shared/models'
9import { createSafeIn, parseRowCountResult } from '../../../shared'
8import { AbstractRunQuery } from '../../../shared/abstract-run-query' 10import { AbstractRunQuery } from '../../../shared/abstract-run-query'
9import { forceNumber } from '@shared/core-utils'
10 11
11/** 12/**
12 * 13 *
@@ -665,7 +666,7 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery {
665 } 666 }
666 667
667 private buildOrder (value: string) { 668 private buildOrder (value: string) {
668 const { direction, field } = buildDirectionAndField(value) 669 const { direction, field } = buildSortDirectionAndField(value)
669 if (field.match(/^[a-zA-Z."]+$/) === null) throw new Error('Invalid sort column ' + field) 670 if (field.match(/^[a-zA-Z."]+$/) === null) throw new Error('Invalid sort column ' + field)
670 671
671 if (field.toLowerCase() === 'random') return 'ORDER BY RANDOM()' 672 if (field.toLowerCase() === 'random') return 'ORDER BY RANDOM()'
diff --git a/server/models/video/tag.ts b/server/models/video/tag.ts
index 653b9694b..cebde3755 100644
--- a/server/models/video/tag.ts
+++ b/server/models/video/tag.ts
@@ -4,7 +4,7 @@ import { MTag } from '@server/types/models'
4import { AttributesOnly } from '@shared/typescript-utils' 4import { AttributesOnly } from '@shared/typescript-utils'
5import { VideoPrivacy, VideoState } from '../../../shared/models/videos' 5import { VideoPrivacy, VideoState } from '../../../shared/models/videos'
6import { isVideoTagValid } from '../../helpers/custom-validators/videos' 6import { isVideoTagValid } from '../../helpers/custom-validators/videos'
7import { throwIfNotValid } from '../utils' 7import { throwIfNotValid } from '../shared'
8import { VideoModel } from './video' 8import { VideoModel } from './video'
9import { VideoTagModel } from './video-tag' 9import { VideoTagModel } from './video-tag'
10 10
diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts
index 1cd8224c0..9247d0e2b 100644
--- a/server/models/video/video-blacklist.ts
+++ b/server/models/video/video-blacklist.ts
@@ -5,7 +5,7 @@ import { AttributesOnly } from '@shared/typescript-utils'
5import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos' 5import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos'
6import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist' 6import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist'
7import { CONSTRAINTS_FIELDS } from '../../initializers/constants' 7import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
8import { getBlacklistSort, searchAttribute, SortType, throwIfNotValid } from '../utils' 8import { getBlacklistSort, searchAttribute, throwIfNotValid } from '../shared'
9import { ThumbnailModel } from './thumbnail' 9import { ThumbnailModel } from './thumbnail'
10import { VideoModel } from './video' 10import { VideoModel } from './video'
11import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' 11import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
@@ -57,7 +57,7 @@ export class VideoBlacklistModel extends Model<Partial<AttributesOnly<VideoBlack
57 static listForApi (parameters: { 57 static listForApi (parameters: {
58 start: number 58 start: number
59 count: number 59 count: number
60 sort: SortType 60 sort: string
61 search?: string 61 search?: string
62 type?: VideoBlacklistType 62 type?: VideoBlacklistType
63 }) { 63 }) {
@@ -67,7 +67,7 @@ export class VideoBlacklistModel extends Model<Partial<AttributesOnly<VideoBlack
67 return { 67 return {
68 offset: start, 68 offset: start,
69 limit: count, 69 limit: count,
70 order: getBlacklistSort(sort.sortModel, sort.sortValue) 70 order: getBlacklistSort(sort)
71 } 71 }
72 } 72 }
73 73
diff --git a/server/models/video/video-caption.ts b/server/models/video/video-caption.ts
index 5fbcd6e3b..2eaa77407 100644
--- a/server/models/video/video-caption.ts
+++ b/server/models/video/video-caption.ts
@@ -23,7 +23,7 @@ import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/vid
23import { logger } from '../../helpers/logger' 23import { logger } from '../../helpers/logger'
24import { CONFIG } from '../../initializers/config' 24import { CONFIG } from '../../initializers/config'
25import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, VIDEO_LANGUAGES, WEBSERVER } from '../../initializers/constants' 25import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, VIDEO_LANGUAGES, WEBSERVER } from '../../initializers/constants'
26import { buildWhereIdOrUUID, throwIfNotValid } from '../utils' 26import { buildWhereIdOrUUID, throwIfNotValid } from '../shared'
27import { VideoModel } from './video' 27import { VideoModel } from './video'
28 28
29export enum ScopeNames { 29export enum ScopeNames {
diff --git a/server/models/video/video-change-ownership.ts b/server/models/video/video-change-ownership.ts
index 1a1b8c88d..2db4b523a 100644
--- a/server/models/video/video-change-ownership.ts
+++ b/server/models/video/video-change-ownership.ts
@@ -3,7 +3,7 @@ import { MVideoChangeOwnershipFormattable, MVideoChangeOwnershipFull } from '@se
3import { AttributesOnly } from '@shared/typescript-utils' 3import { AttributesOnly } from '@shared/typescript-utils'
4import { VideoChangeOwnership, VideoChangeOwnershipStatus } from '../../../shared/models/videos' 4import { VideoChangeOwnership, VideoChangeOwnershipStatus } from '../../../shared/models/videos'
5import { AccountModel } from '../account/account' 5import { AccountModel } from '../account/account'
6import { getSort } from '../utils' 6import { getSort } from '../shared'
7import { ScopeNames as VideoScopeNames, VideoModel } from './video' 7import { ScopeNames as VideoScopeNames, VideoModel } from './video'
8 8
9enum ScopeNames { 9enum ScopeNames {
diff --git a/server/models/video/video-channel-sync.ts b/server/models/video/video-channel-sync.ts
index 6e49cde10..a4cbf51f5 100644
--- a/server/models/video/video-channel-sync.ts
+++ b/server/models/video/video-channel-sync.ts
@@ -21,7 +21,7 @@ import { VideoChannelSync, VideoChannelSyncState } from '@shared/models'
21import { AttributesOnly } from '@shared/typescript-utils' 21import { AttributesOnly } from '@shared/typescript-utils'
22import { AccountModel } from '../account/account' 22import { AccountModel } from '../account/account'
23import { UserModel } from '../user/user' 23import { UserModel } from '../user/user'
24import { getChannelSyncSort, throwIfNotValid } from '../utils' 24import { getChannelSyncSort, throwIfNotValid } from '../shared'
25import { VideoChannelModel } from './video-channel' 25import { VideoChannelModel } from './video-channel'
26 26
27@DefaultScope(() => ({ 27@DefaultScope(() => ({
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index 132c8f021..b71f5a197 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -43,8 +43,14 @@ import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor'
43import { ActorFollowModel } from '../actor/actor-follow' 43import { ActorFollowModel } from '../actor/actor-follow'
44import { ActorImageModel } from '../actor/actor-image' 44import { ActorImageModel } from '../actor/actor-image'
45import { ServerModel } from '../server/server' 45import { ServerModel } from '../server/server'
46import { setAsUpdated } from '../shared' 46import {
47import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' 47 buildServerIdsFollowedBy,
48 buildTrigramSearchIndex,
49 createSimilarityAttribute,
50 getSort,
51 setAsUpdated,
52 throwIfNotValid
53} from '../shared'
48import { VideoModel } from './video' 54import { VideoModel } from './video'
49import { VideoPlaylistModel } from './video-playlist' 55import { VideoPlaylistModel } from './video-playlist'
50 56
@@ -831,6 +837,6 @@ export class VideoChannelModel extends Model<Partial<AttributesOnly<VideoChannel
831 } 837 }
832 838
833 setAsUpdated (transaction?: Transaction) { 839 setAsUpdated (transaction?: Transaction) {
834 return setAsUpdated('videoChannel', this.id, transaction) 840 return setAsUpdated({ sequelize: this.sequelize, table: 'videoChannel', id: this.id, transaction })
835 } 841 }
836} 842}
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts
index af9614d30..ff5142809 100644
--- a/server/models/video/video-comment.ts
+++ b/server/models/video/video-comment.ts
@@ -1,4 +1,4 @@
1import { FindOptions, Op, Order, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' 1import { FindOptions, Op, Order, QueryTypes, Sequelize, Transaction } from 'sequelize'
2import { 2import {
3 AllowNull, 3 AllowNull,
4 BelongsTo, 4 BelongsTo,
@@ -13,11 +13,9 @@ import {
13 Table, 13 Table,
14 UpdatedAt 14 UpdatedAt
15} from 'sequelize-typescript' 15} from 'sequelize-typescript'
16import { exists } from '@server/helpers/custom-validators/misc'
17import { getServerActor } from '@server/models/application/application' 16import { getServerActor } from '@server/models/application/application'
18import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' 17import { MAccount, MAccountId, MUserAccountId } from '@server/types/models'
19import { uniqify } from '@shared/core-utils' 18import { pick, uniqify } from '@shared/core-utils'
20import { VideoPrivacy } from '@shared/models'
21import { AttributesOnly } from '@shared/typescript-utils' 19import { AttributesOnly } from '@shared/typescript-utils'
22import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects' 20import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects'
23import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' 21import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object'
@@ -41,61 +39,19 @@ import {
41} from '../../types/models/video' 39} from '../../types/models/video'
42import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' 40import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
43import { AccountModel } from '../account/account' 41import { AccountModel } from '../account/account'
44import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor' 42import { ActorModel } from '../actor/actor'
45import { 43import { buildLocalAccountIdsIn, buildSQLAttributes, throwIfNotValid } from '../shared'
46 buildBlockedAccountSQL, 44import { ListVideoCommentsOptions, VideoCommentListQueryBuilder } from './sql/comment/video-comment-list-query-builder'
47 buildBlockedAccountSQLOptimized,
48 buildLocalAccountIdsIn,
49 getCommentSort,
50 searchAttribute,
51 throwIfNotValid
52} from '../utils'
53import { VideoModel } from './video' 45import { VideoModel } from './video'
54import { VideoChannelModel } from './video-channel' 46import { VideoChannelModel } from './video-channel'
55 47
56export enum ScopeNames { 48export enum ScopeNames {
57 WITH_ACCOUNT = 'WITH_ACCOUNT', 49 WITH_ACCOUNT = 'WITH_ACCOUNT',
58 WITH_ACCOUNT_FOR_API = 'WITH_ACCOUNT_FOR_API',
59 WITH_IN_REPLY_TO = 'WITH_IN_REPLY_TO', 50 WITH_IN_REPLY_TO = 'WITH_IN_REPLY_TO',
60 WITH_VIDEO = 'WITH_VIDEO', 51 WITH_VIDEO = 'WITH_VIDEO'
61 ATTRIBUTES_FOR_API = 'ATTRIBUTES_FOR_API'
62} 52}
63 53
64@Scopes(() => ({ 54@Scopes(() => ({
65 [ScopeNames.ATTRIBUTES_FOR_API]: (blockerAccountIds: number[]) => {
66 return {
67 attributes: {
68 include: [
69 [
70 Sequelize.literal(
71 '(' +
72 'WITH "blocklist" AS (' + buildBlockedAccountSQL(blockerAccountIds) + ')' +
73 'SELECT COUNT("replies"."id") ' +
74 'FROM "videoComment" AS "replies" ' +
75 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' +
76 'AND "deletedAt" IS NULL ' +
77 'AND "accountId" NOT IN (SELECT "id" FROM "blocklist")' +
78 ')'
79 ),
80 'totalReplies'
81 ],
82 [
83 Sequelize.literal(
84 '(' +
85 'SELECT COUNT("replies"."id") ' +
86 'FROM "videoComment" AS "replies" ' +
87 'INNER JOIN "video" ON "video"."id" = "replies"."videoId" ' +
88 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
89 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' +
90 'AND "replies"."accountId" = "videoChannel"."accountId"' +
91 ')'
92 ),
93 'totalRepliesFromVideoAuthor'
94 ]
95 ]
96 }
97 } as FindOptions
98 },
99 [ScopeNames.WITH_ACCOUNT]: { 55 [ScopeNames.WITH_ACCOUNT]: {
100 include: [ 56 include: [
101 { 57 {
@@ -103,22 +59,6 @@ export enum ScopeNames {
103 } 59 }
104 ] 60 ]
105 }, 61 },
106 [ScopeNames.WITH_ACCOUNT_FOR_API]: {
107 include: [
108 {
109 model: AccountModel.unscoped(),
110 include: [
111 {
112 attributes: {
113 exclude: unusedActorAttributesForAPI
114 },
115 model: ActorModel, // Default scope includes avatar and server
116 required: true
117 }
118 ]
119 }
120 ]
121 },
122 [ScopeNames.WITH_IN_REPLY_TO]: { 62 [ScopeNames.WITH_IN_REPLY_TO]: {
123 include: [ 63 include: [
124 { 64 {
@@ -252,6 +192,18 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment
252 }) 192 })
253 CommentAbuses: VideoCommentAbuseModel[] 193 CommentAbuses: VideoCommentAbuseModel[]
254 194
195 // ---------------------------------------------------------------------------
196
197 static getSQLAttributes (tableName: string, aliasPrefix = '') {
198 return buildSQLAttributes({
199 model: this,
200 tableName,
201 aliasPrefix
202 })
203 }
204
205 // ---------------------------------------------------------------------------
206
255 static loadById (id: number, t?: Transaction): Promise<MComment> { 207 static loadById (id: number, t?: Transaction): Promise<MComment> {
256 const query: FindOptions = { 208 const query: FindOptions = {
257 where: { 209 where: {
@@ -319,93 +271,19 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment
319 searchAccount?: string 271 searchAccount?: string
320 searchVideo?: string 272 searchVideo?: string
321 }) { 273 }) {
322 const { start, count, sort, isLocal, search, searchAccount, searchVideo, onLocalVideo } = parameters 274 const queryOptions: ListVideoCommentsOptions = {
275 ...pick(parameters, [ 'start', 'count', 'sort', 'isLocal', 'search', 'searchVideo', 'searchAccount', 'onLocalVideo' ]),
323 276
324 const where: WhereOptions = { 277 selectType: 'api',
325 deletedAt: null 278 notDeleted: true
326 }
327
328 const whereAccount: WhereOptions = {}
329 const whereActor: WhereOptions = {}
330 const whereVideo: WhereOptions = {}
331
332 if (isLocal === true) {
333 Object.assign(whereActor, {
334 serverId: null
335 })
336 } else if (isLocal === false) {
337 Object.assign(whereActor, {
338 serverId: {
339 [Op.ne]: null
340 }
341 })
342 }
343
344 if (search) {
345 Object.assign(where, {
346 [Op.or]: [
347 searchAttribute(search, 'text'),
348 searchAttribute(search, '$Account.Actor.preferredUsername$'),
349 searchAttribute(search, '$Account.name$'),
350 searchAttribute(search, '$Video.name$')
351 ]
352 })
353 }
354
355 if (searchAccount) {
356 Object.assign(whereActor, {
357 [Op.or]: [
358 searchAttribute(searchAccount, '$Account.Actor.preferredUsername$'),
359 searchAttribute(searchAccount, '$Account.name$')
360 ]
361 })
362 }
363
364 if (searchVideo) {
365 Object.assign(whereVideo, searchAttribute(searchVideo, 'name'))
366 }
367
368 if (exists(onLocalVideo)) {
369 Object.assign(whereVideo, { remote: !onLocalVideo })
370 }
371
372 const getQuery = (forCount: boolean) => {
373 return {
374 offset: start,
375 limit: count,
376 order: getCommentSort(sort),
377 where,
378 include: [
379 {
380 model: AccountModel.unscoped(),
381 required: true,
382 where: whereAccount,
383 include: [
384 {
385 attributes: {
386 exclude: unusedActorAttributesForAPI
387 },
388 model: forCount === true
389 ? ActorModel.unscoped() // Default scope includes avatar and server
390 : ActorModel,
391 required: true,
392 where: whereActor
393 }
394 ]
395 },
396 {
397 model: VideoModel.unscoped(),
398 required: true,
399 where: whereVideo
400 }
401 ]
402 }
403 } 279 }
404 280
405 return Promise.all([ 281 return Promise.all([
406 VideoCommentModel.count(getQuery(true)), 282 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MCommentAdminFormattable>(),
407 VideoCommentModel.findAll(getQuery(false)) 283 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).countComments()
408 ]).then(([ total, data ]) => ({ total, data })) 284 ]).then(([ rows, count ]) => {
285 return { total: count, data: rows }
286 })
409 } 287 }
410 288
411 static async listThreadsForApi (parameters: { 289 static async listThreadsForApi (parameters: {
@@ -416,67 +294,40 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment
416 sort: string 294 sort: string
417 user?: MUserAccountId 295 user?: MUserAccountId
418 }) { 296 }) {
419 const { videoId, isVideoOwned, start, count, sort, user } = parameters 297 const { videoId, user } = parameters
420 298
421 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ videoId, user, isVideoOwned }) 299 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user })
422 300
423 const accountBlockedWhere = { 301 const commonOptions: ListVideoCommentsOptions = {
424 accountId: { 302 selectType: 'api',
425 [Op.notIn]: Sequelize.literal( 303 videoId,
426 '(' + buildBlockedAccountSQL(blockerAccountIds) + ')' 304 blockerAccountIds
427 )
428 }
429 } 305 }
430 306
431 const queryList = { 307 const listOptions: ListVideoCommentsOptions = {
432 offset: start, 308 ...commonOptions,
433 limit: count, 309 ...pick(parameters, [ 'sort', 'start', 'count' ]),
434 order: getCommentSort(sort), 310
435 where: { 311 isThread: true,
436 [Op.and]: [ 312 includeReplyCounters: true
437 {
438 videoId
439 },
440 {
441 inReplyToCommentId: null
442 },
443 {
444 [Op.or]: [
445 accountBlockedWhere,
446 {
447 accountId: null
448 }
449 ]
450 }
451 ]
452 }
453 } 313 }
454 314
455 const findScopesList: (string | ScopeOptions)[] = [ 315 const countOptions: ListVideoCommentsOptions = {
456 ScopeNames.WITH_ACCOUNT_FOR_API, 316 ...commonOptions,
457 {
458 method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ]
459 }
460 ]
461 317
462 const countScopesList: ScopeOptions[] = [ 318 isThread: true
463 { 319 }
464 method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ]
465 }
466 ]
467 320
468 const notDeletedQueryCount = { 321 const notDeletedCountOptions: ListVideoCommentsOptions = {
469 where: { 322 ...commonOptions,
470 videoId, 323
471 deletedAt: null, 324 notDeleted: true
472 ...accountBlockedWhere
473 }
474 } 325 }
475 326
476 return Promise.all([ 327 return Promise.all([
477 VideoCommentModel.scope(findScopesList).findAll(queryList), 328 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, listOptions).listComments<MCommentAdminFormattable>(),
478 VideoCommentModel.scope(countScopesList).count(queryList), 329 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, countOptions).countComments(),
479 VideoCommentModel.count(notDeletedQueryCount) 330 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, notDeletedCountOptions).countComments()
480 ]).then(([ rows, count, totalNotDeletedComments ]) => { 331 ]).then(([ rows, count, totalNotDeletedComments ]) => {
481 return { total: count, data: rows, totalNotDeletedComments } 332 return { total: count, data: rows, totalNotDeletedComments }
482 }) 333 })
@@ -484,54 +335,29 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment
484 335
485 static async listThreadCommentsForApi (parameters: { 336 static async listThreadCommentsForApi (parameters: {
486 videoId: number 337 videoId: number
487 isVideoOwned: boolean
488 threadId: number 338 threadId: number
489 user?: MUserAccountId 339 user?: MUserAccountId
490 }) { 340 }) {
491 const { videoId, threadId, user, isVideoOwned } = parameters 341 const { user } = parameters
492 342
493 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ videoId, user, isVideoOwned }) 343 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user })
494 344
495 const query = { 345 const queryOptions: ListVideoCommentsOptions = {
496 order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ] as Order, 346 ...pick(parameters, [ 'videoId', 'threadId' ]),
497 where: {
498 videoId,
499 [Op.and]: [
500 {
501 [Op.or]: [
502 { id: threadId },
503 { originCommentId: threadId }
504 ]
505 },
506 {
507 [Op.or]: [
508 {
509 accountId: {
510 [Op.notIn]: Sequelize.literal(
511 '(' + buildBlockedAccountSQL(blockerAccountIds) + ')'
512 )
513 }
514 },
515 {
516 accountId: null
517 }
518 ]
519 }
520 ]
521 }
522 }
523 347
524 const scopes: any[] = [ 348 selectType: 'api',
525 ScopeNames.WITH_ACCOUNT_FOR_API, 349 sort: 'createdAt',
526 { 350
527 method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ] 351 blockerAccountIds,
528 } 352 includeReplyCounters: true
529 ] 353 }
530 354
531 return Promise.all([ 355 return Promise.all([
532 VideoCommentModel.count(query), 356 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MCommentAdminFormattable>(),
533 VideoCommentModel.scope(scopes).findAll(query) 357 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).countComments()
534 ]).then(([ total, data ]) => ({ total, data })) 358 ]).then(([ rows, count ]) => {
359 return { total: count, data: rows }
360 })
535 } 361 }
536 362
537 static listThreadParentComments (comment: MCommentId, t: Transaction, order: 'ASC' | 'DESC' = 'ASC'): Promise<MCommentOwner[]> { 363 static listThreadParentComments (comment: MCommentId, t: Transaction, order: 'ASC' | 'DESC' = 'ASC'): Promise<MCommentOwner[]> {
@@ -559,31 +385,31 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment
559 .findAll(query) 385 .findAll(query)
560 } 386 }
561 387
562 static async listAndCountByVideoForAP (video: MVideoImmutable, start: number, count: number, t?: Transaction) { 388 static async listAndCountByVideoForAP (parameters: {
563 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ 389 video: MVideoImmutable
390 start: number
391 count: number
392 }) {
393 const { video } = parameters
394
395 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user: null })
396
397 const queryOptions: ListVideoCommentsOptions = {
398 ...pick(parameters, [ 'start', 'count' ]),
399
400 selectType: 'comment-only',
564 videoId: video.id, 401 videoId: video.id,
565 isVideoOwned: video.isOwned() 402 sort: 'createdAt',
566 })
567 403
568 const query = { 404 blockerAccountIds
569 order: [ [ 'createdAt', 'ASC' ] ] as Order,
570 offset: start,
571 limit: count,
572 where: {
573 videoId: video.id,
574 accountId: {
575 [Op.notIn]: Sequelize.literal(
576 '(' + buildBlockedAccountSQL(blockerAccountIds) + ')'
577 )
578 }
579 },
580 transaction: t
581 } 405 }
582 406
583 return Promise.all([ 407 return Promise.all([
584 VideoCommentModel.count(query), 408 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MComment>(),
585 VideoCommentModel.findAll<MComment>(query) 409 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).countComments()
586 ]).then(([ total, data ]) => ({ total, data })) 410 ]).then(([ rows, count ]) => {
411 return { total: count, data: rows }
412 })
587 } 413 }
588 414
589 static async listForFeed (parameters: { 415 static async listForFeed (parameters: {
@@ -592,97 +418,36 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment
592 videoId?: number 418 videoId?: number
593 accountId?: number 419 accountId?: number
594 videoChannelId?: number 420 videoChannelId?: number
595 }): Promise<MCommentOwnerVideoFeed[]> { 421 }) {
596 const serverActor = await getServerActor() 422 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user: null })
597 const { start, count, videoId, accountId, videoChannelId } = parameters
598
599 const whereAnd: WhereOptions[] = buildBlockedAccountSQLOptimized(
600 '"VideoCommentModel"."accountId"',
601 [ serverActor.Account.id, '"Video->VideoChannel"."accountId"' ]
602 )
603 423
604 if (accountId) { 424 const queryOptions: ListVideoCommentsOptions = {
605 whereAnd.push({ 425 ...pick(parameters, [ 'start', 'count', 'accountId', 'videoId', 'videoChannelId' ]),
606 accountId
607 })
608 }
609 426
610 const accountWhere = { 427 selectType: 'feed',
611 [Op.and]: whereAnd
612 }
613 428
614 const videoChannelWhere = videoChannelId ? { id: videoChannelId } : undefined 429 sort: '-createdAt',
430 onPublicVideo: true,
431 notDeleted: true,
615 432
616 const query = { 433 blockerAccountIds
617 order: [ [ 'createdAt', 'DESC' ] ] as Order,
618 offset: start,
619 limit: count,
620 where: {
621 deletedAt: null,
622 accountId: accountWhere
623 },
624 include: [
625 {
626 attributes: [ 'name', 'uuid' ],
627 model: VideoModel.unscoped(),
628 required: true,
629 where: {
630 privacy: VideoPrivacy.PUBLIC
631 },
632 include: [
633 {
634 attributes: [ 'accountId' ],
635 model: VideoChannelModel.unscoped(),
636 required: true,
637 where: videoChannelWhere
638 }
639 ]
640 }
641 ]
642 } 434 }
643 435
644 if (videoId) query.where['videoId'] = videoId 436 return new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MCommentOwnerVideoFeed>()
645
646 return VideoCommentModel
647 .scope([ ScopeNames.WITH_ACCOUNT ])
648 .findAll(query)
649 } 437 }
650 438
651 static listForBulkDelete (ofAccount: MAccount, filter: { onVideosOfAccount?: MAccountId } = {}) { 439 static listForBulkDelete (ofAccount: MAccount, filter: { onVideosOfAccount?: MAccountId } = {}) {
652 const accountWhere = filter.onVideosOfAccount 440 const queryOptions: ListVideoCommentsOptions = {
653 ? { id: filter.onVideosOfAccount.id } 441 selectType: 'comment-only',
654 : {}
655 442
656 const query = { 443 accountId: ofAccount.id,
657 limit: 1000, 444 videoAccountOwnerId: filter.onVideosOfAccount?.id,
658 where: { 445
659 deletedAt: null, 446 notDeleted: true,
660 accountId: ofAccount.id 447 count: 5000
661 },
662 include: [
663 {
664 model: VideoModel,
665 required: true,
666 include: [
667 {
668 model: VideoChannelModel,
669 required: true,
670 include: [
671 {
672 model: AccountModel,
673 required: true,
674 where: accountWhere
675 }
676 ]
677 }
678 ]
679 }
680 ]
681 } 448 }
682 449
683 return VideoCommentModel 450 return new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MComment>()
684 .scope([ ScopeNames.WITH_ACCOUNT ])
685 .findAll(query)
686 } 451 }
687 452
688 static async getStats () { 453 static async getStats () {
@@ -750,9 +515,7 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment
750 } 515 }
751 516
752 isOwned () { 517 isOwned () {
753 if (!this.Account) { 518 if (!this.Account) return false
754 return false
755 }
756 519
757 return this.Account.isOwned() 520 return this.Account.isOwned()
758 } 521 }
@@ -906,22 +669,15 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment
906 } 669 }
907 670
908 private static async buildBlockerAccountIds (options: { 671 private static async buildBlockerAccountIds (options: {
909 videoId: number 672 user: MUserAccountId
910 isVideoOwned: boolean 673 }): Promise<number[]> {
911 user?: MUserAccountId 674 const { user } = options
912 }) {
913 const { videoId, user, isVideoOwned } = options
914 675
915 const serverActor = await getServerActor() 676 const serverActor = await getServerActor()
916 const blockerAccountIds = [ serverActor.Account.id ] 677 const blockerAccountIds = [ serverActor.Account.id ]
917 678
918 if (user) blockerAccountIds.push(user.Account.id) 679 if (user) blockerAccountIds.push(user.Account.id)
919 680
920 if (isVideoOwned) {
921 const videoOwnerAccount = await AccountModel.loadAccountIdFromVideo(videoId)
922 if (videoOwnerAccount) blockerAccountIds.push(videoOwnerAccount.id)
923 }
924
925 return blockerAccountIds 681 return blockerAccountIds
926 } 682 }
927} 683}
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts
index 9c4e6d078..07bc13de1 100644
--- a/server/models/video/video-file.ts
+++ b/server/models/video/video-file.ts
@@ -21,6 +21,7 @@ import {
21import validator from 'validator' 21import validator from 'validator'
22import { logger } from '@server/helpers/logger' 22import { logger } from '@server/helpers/logger'
23import { extractVideo } from '@server/helpers/video' 23import { extractVideo } from '@server/helpers/video'
24import { CONFIG } from '@server/initializers/config'
24import { buildRemoteVideoBaseUrl } from '@server/lib/activitypub/url' 25import { buildRemoteVideoBaseUrl } from '@server/lib/activitypub/url'
25import { 26import {
26 getHLSPrivateFileUrl, 27 getHLSPrivateFileUrl,
@@ -50,11 +51,9 @@ import {
50} from '../../initializers/constants' 51} from '../../initializers/constants'
51import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file' 52import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file'
52import { VideoRedundancyModel } from '../redundancy/video-redundancy' 53import { VideoRedundancyModel } from '../redundancy/video-redundancy'
53import { doesExist } from '../shared' 54import { doesExist, parseAggregateResult, throwIfNotValid } from '../shared'
54import { parseAggregateResult, throwIfNotValid } from '../utils'
55import { VideoModel } from './video' 55import { VideoModel } from './video'
56import { VideoStreamingPlaylistModel } from './video-streaming-playlist' 56import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
57import { CONFIG } from '@server/initializers/config'
58 57
59export enum ScopeNames { 58export enum ScopeNames {
60 WITH_VIDEO = 'WITH_VIDEO', 59 WITH_VIDEO = 'WITH_VIDEO',
@@ -266,7 +265,7 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel>
266 static doesInfohashExist (infoHash: string) { 265 static doesInfohashExist (infoHash: string) {
267 const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1' 266 const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1'
268 267
269 return doesExist(query, { infoHash }) 268 return doesExist(this.sequelize, query, { infoHash })
270 } 269 }
271 270
272 static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) { 271 static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) {
@@ -282,14 +281,14 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel>
282 'LEFT JOIN "video" "hlsVideo" ON "hlsVideo"."id" = "videoStreamingPlaylist"."videoId" AND "hlsVideo"."remote" IS FALSE ' + 281 'LEFT JOIN "video" "hlsVideo" ON "hlsVideo"."id" = "videoStreamingPlaylist"."videoId" AND "hlsVideo"."remote" IS FALSE ' +
283 'WHERE "torrentFilename" = $filename AND ("hlsVideo"."id" IS NOT NULL OR "webtorrent"."id" IS NOT NULL) LIMIT 1' 282 'WHERE "torrentFilename" = $filename AND ("hlsVideo"."id" IS NOT NULL OR "webtorrent"."id" IS NOT NULL) LIMIT 1'
284 283
285 return doesExist(query, { filename }) 284 return doesExist(this.sequelize, query, { filename })
286 } 285 }
287 286
288 static async doesOwnedWebTorrentVideoFileExist (filename: string) { 287 static async doesOwnedWebTorrentVideoFileExist (filename: string) {
289 const query = 'SELECT 1 FROM "videoFile" INNER JOIN "video" ON "video"."id" = "videoFile"."videoId" AND "video"."remote" IS FALSE ' + 288 const query = 'SELECT 1 FROM "videoFile" INNER JOIN "video" ON "video"."id" = "videoFile"."videoId" AND "video"."remote" IS FALSE ' +
290 `WHERE "filename" = $filename AND "storage" = ${VideoStorage.FILE_SYSTEM} LIMIT 1` 289 `WHERE "filename" = $filename AND "storage" = ${VideoStorage.FILE_SYSTEM} LIMIT 1`
291 290
292 return doesExist(query, { filename }) 291 return doesExist(this.sequelize, query, { filename })
293 } 292 }
294 293
295 static loadByFilename (filename: string) { 294 static loadByFilename (filename: string) {
@@ -439,7 +438,7 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel>
439 if (!element) return videoFile.save({ transaction }) 438 if (!element) return videoFile.save({ transaction })
440 439
441 for (const k of Object.keys(videoFile.toJSON())) { 440 for (const k of Object.keys(videoFile.toJSON())) {
442 element[k] = videoFile[k] 441 element.set(k, videoFile[k])
443 } 442 }
444 443
445 return element.save({ transaction }) 444 return element.save({ transaction })
diff --git a/server/models/video/video-import.ts b/server/models/video/video-import.ts
index da6b92c7a..c040e0fda 100644
--- a/server/models/video/video-import.ts
+++ b/server/models/video/video-import.ts
@@ -22,7 +22,7 @@ import { isVideoImportStateValid, isVideoImportTargetUrlValid } from '../../help
22import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos' 22import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos'
23import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers/constants' 23import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers/constants'
24import { UserModel } from '../user/user' 24import { UserModel } from '../user/user'
25import { getSort, searchAttribute, throwIfNotValid } from '../utils' 25import { getSort, searchAttribute, throwIfNotValid } from '../shared'
26import { ScopeNames as VideoModelScopeNames, VideoModel } from './video' 26import { ScopeNames as VideoModelScopeNames, VideoModel } from './video'
27import { VideoChannelSyncModel } from './video-channel-sync' 27import { VideoChannelSyncModel } from './video-channel-sync'
28 28
diff --git a/server/models/video/video-playlist-element.ts b/server/models/video/video-playlist-element.ts
index 7181b5599..b832f9768 100644
--- a/server/models/video/video-playlist-element.ts
+++ b/server/models/video/video-playlist-element.ts
@@ -31,7 +31,7 @@ import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../shared/
31import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 31import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
32import { CONSTRAINTS_FIELDS } from '../../initializers/constants' 32import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
33import { AccountModel } from '../account/account' 33import { AccountModel } from '../account/account'
34import { getSort, throwIfNotValid } from '../utils' 34import { getSort, throwIfNotValid } from '../shared'
35import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video' 35import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video'
36import { VideoPlaylistModel } from './video-playlist' 36import { VideoPlaylistModel } from './video-playlist'
37 37
@@ -309,7 +309,23 @@ export class VideoPlaylistElementModel extends Model<Partial<AttributesOnly<Vide
309 return VideoPlaylistElementModel.increment({ position: by }, query) 309 return VideoPlaylistElementModel.increment({ position: by }, query)
310 } 310 }
311 311
312 getType (this: MVideoPlaylistElementFormattable, displayNSFW?: boolean, accountId?: number) { 312 toFormattedJSON (
313 this: MVideoPlaylistElementFormattable,
314 options: { accountId?: number } = {}
315 ): VideoPlaylistElement {
316 return {
317 id: this.id,
318 position: this.position,
319 startTimestamp: this.startTimestamp,
320 stopTimestamp: this.stopTimestamp,
321
322 type: this.getType(options.accountId),
323
324 video: this.getVideoElement(options.accountId)
325 }
326 }
327
328 getType (this: MVideoPlaylistElementFormattable, accountId?: number) {
313 const video = this.Video 329 const video = this.Video
314 330
315 if (!video) return VideoPlaylistElementType.DELETED 331 if (!video) return VideoPlaylistElementType.DELETED
@@ -323,34 +339,17 @@ export class VideoPlaylistElementModel extends Model<Partial<AttributesOnly<Vide
323 if (video.privacy === VideoPrivacy.PRIVATE || video.privacy === VideoPrivacy.INTERNAL) return VideoPlaylistElementType.PRIVATE 339 if (video.privacy === VideoPrivacy.PRIVATE || video.privacy === VideoPrivacy.INTERNAL) return VideoPlaylistElementType.PRIVATE
324 340
325 if (video.isBlacklisted() || video.isBlocked()) return VideoPlaylistElementType.UNAVAILABLE 341 if (video.isBlacklisted() || video.isBlocked()) return VideoPlaylistElementType.UNAVAILABLE
326 if (video.nsfw === true && displayNSFW === false) return VideoPlaylistElementType.UNAVAILABLE
327 342
328 return VideoPlaylistElementType.REGULAR 343 return VideoPlaylistElementType.REGULAR
329 } 344 }
330 345
331 getVideoElement (this: MVideoPlaylistElementFormattable, displayNSFW?: boolean, accountId?: number) { 346 getVideoElement (this: MVideoPlaylistElementFormattable, accountId?: number) {
332 if (!this.Video) return null 347 if (!this.Video) return null
333 if (this.getType(displayNSFW, accountId) !== VideoPlaylistElementType.REGULAR) return null 348 if (this.getType(accountId) !== VideoPlaylistElementType.REGULAR) return null
334 349
335 return this.Video.toFormattedJSON() 350 return this.Video.toFormattedJSON()
336 } 351 }
337 352
338 toFormattedJSON (
339 this: MVideoPlaylistElementFormattable,
340 options: { displayNSFW?: boolean, accountId?: number } = {}
341 ): VideoPlaylistElement {
342 return {
343 id: this.id,
344 position: this.position,
345 startTimestamp: this.startTimestamp,
346 stopTimestamp: this.stopTimestamp,
347
348 type: this.getType(options.displayNSFW, options.accountId),
349
350 video: this.getVideoElement(options.displayNSFW, options.accountId)
351 }
352 }
353
354 toActivityPubObject (this: MVideoPlaylistElementAP): PlaylistElementObject { 353 toActivityPubObject (this: MVideoPlaylistElementAP): PlaylistElementObject {
355 const base: PlaylistElementObject = { 354 const base: PlaylistElementObject = {
356 id: this.url, 355 id: this.url,
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts
index 8bbe54c49..faf4bea78 100644
--- a/server/models/video/video-playlist.ts
+++ b/server/models/video/video-playlist.ts
@@ -21,12 +21,8 @@ import { activityPubCollectionPagination } from '@server/lib/activitypub/collect
21import { MAccountId, MChannelId } from '@server/types/models' 21import { MAccountId, MChannelId } from '@server/types/models'
22import { buildPlaylistEmbedPath, buildPlaylistWatchPath, pick } from '@shared/core-utils' 22import { buildPlaylistEmbedPath, buildPlaylistWatchPath, pick } from '@shared/core-utils'
23import { buildUUID, uuidToShort } from '@shared/extra-utils' 23import { buildUUID, uuidToShort } from '@shared/extra-utils'
24import { ActivityIconObject, PlaylistObject, VideoPlaylist, VideoPlaylistPrivacy, VideoPlaylistType } from '@shared/models'
24import { AttributesOnly } from '@shared/typescript-utils' 25import { AttributesOnly } from '@shared/typescript-utils'
25import { ActivityIconObject } from '../../../shared/models/activitypub/objects'
26import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object'
27import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
28import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model'
29import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model'
30import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 26import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
31import { 27import {
32 isVideoPlaylistDescriptionValid, 28 isVideoPlaylistDescriptionValid,
@@ -53,7 +49,6 @@ import {
53} from '../../types/models/video/video-playlist' 49} from '../../types/models/video/video-playlist'
54import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' 50import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account'
55import { ActorModel } from '../actor/actor' 51import { ActorModel } from '../actor/actor'
56import { setAsUpdated } from '../shared'
57import { 52import {
58 buildServerIdsFollowedBy, 53 buildServerIdsFollowedBy,
59 buildTrigramSearchIndex, 54 buildTrigramSearchIndex,
@@ -61,8 +56,9 @@ import {
61 createSimilarityAttribute, 56 createSimilarityAttribute,
62 getPlaylistSort, 57 getPlaylistSort,
63 isOutdated, 58 isOutdated,
59 setAsUpdated,
64 throwIfNotValid 60 throwIfNotValid
65} from '../utils' 61} from '../shared'
66import { ThumbnailModel } from './thumbnail' 62import { ThumbnailModel } from './thumbnail'
67import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' 63import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel'
68import { VideoPlaylistElementModel } from './video-playlist-element' 64import { VideoPlaylistElementModel } from './video-playlist-element'
@@ -641,7 +637,7 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli
641 } 637 }
642 638
643 setAsRefreshed () { 639 setAsRefreshed () {
644 return setAsUpdated('videoPlaylist', this.id) 640 return setAsUpdated({ sequelize: this.sequelize, table: 'videoPlaylist', id: this.id })
645 } 641 }
646 642
647 setVideosLength (videosLength: number) { 643 setVideosLength (videosLength: number) {
diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts
index f2190037e..b4de2b20f 100644
--- a/server/models/video/video-share.ts
+++ b/server/models/video/video-share.ts
@@ -7,7 +7,7 @@ import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
7import { MActorDefault, MActorFollowersUrl, MActorId } from '../../types/models' 7import { MActorDefault, MActorFollowersUrl, MActorId } from '../../types/models'
8import { MVideoShareActor, MVideoShareFull } from '../../types/models/video' 8import { MVideoShareActor, MVideoShareFull } from '../../types/models/video'
9import { ActorModel } from '../actor/actor' 9import { ActorModel } from '../actor/actor'
10import { buildLocalActorIdsIn, throwIfNotValid } from '../utils' 10import { buildLocalActorIdsIn, throwIfNotValid } from '../shared'
11import { VideoModel } from './video' 11import { VideoModel } from './video'
12 12
13enum ScopeNames { 13enum ScopeNames {
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts
index 0386edf28..a85c79c9f 100644
--- a/server/models/video/video-streaming-playlist.ts
+++ b/server/models/video/video-streaming-playlist.ts
@@ -37,8 +37,7 @@ import {
37 WEBSERVER 37 WEBSERVER
38} from '../../initializers/constants' 38} from '../../initializers/constants'
39import { VideoRedundancyModel } from '../redundancy/video-redundancy' 39import { VideoRedundancyModel } from '../redundancy/video-redundancy'
40import { doesExist } from '../shared' 40import { doesExist, throwIfNotValid } from '../shared'
41import { throwIfNotValid } from '../utils'
42import { VideoModel } from './video' 41import { VideoModel } from './video'
43 42
44@Table({ 43@Table({
@@ -138,7 +137,7 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
138 static doesInfohashExist (infoHash: string) { 137 static doesInfohashExist (infoHash: string) {
139 const query = 'SELECT 1 FROM "videoStreamingPlaylist" WHERE $infoHash = ANY("p2pMediaLoaderInfohashes") LIMIT 1' 138 const query = 'SELECT 1 FROM "videoStreamingPlaylist" WHERE $infoHash = ANY("p2pMediaLoaderInfohashes") LIMIT 1'
140 139
141 return doesExist(query, { infoHash }) 140 return doesExist(this.sequelize, query, { infoHash })
142 } 141 }
143 142
144 static buildP2PMediaLoaderInfoHashes (playlistUrl: string, files: unknown[]) { 143 static buildP2PMediaLoaderInfoHashes (playlistUrl: string, files: unknown[]) {
@@ -237,7 +236,7 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
237 `AND "video"."remote" IS FALSE AND "video"."uuid" = $videoUUID ` + 236 `AND "video"."remote" IS FALSE AND "video"."uuid" = $videoUUID ` +
238 `AND "storage" = ${VideoStorage.FILE_SYSTEM} LIMIT 1` 237 `AND "storage" = ${VideoStorage.FILE_SYSTEM} LIMIT 1`
239 238
240 return doesExist(query, { videoUUID }) 239 return doesExist(this.sequelize, query, { videoUUID })
241 } 240 }
242 241
243 assignP2PMediaLoaderInfoHashes (video: MVideo, files: unknown[]) { 242 assignP2PMediaLoaderInfoHashes (video: MVideo, files: unknown[]) {
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 56cc45cfe..1a10d2da2 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -32,7 +32,7 @@ import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFil
32import { VideoPathManager } from '@server/lib/video-path-manager' 32import { VideoPathManager } from '@server/lib/video-path-manager'
33import { isVideoInPrivateDirectory } from '@server/lib/video-privacy' 33import { isVideoInPrivateDirectory } from '@server/lib/video-privacy'
34import { getServerActor } from '@server/models/application/application' 34import { getServerActor } from '@server/models/application/application'
35import { ModelCache } from '@server/models/model-cache' 35import { ModelCache } from '@server/models/shared/model-cache'
36import { buildVideoEmbedPath, buildVideoWatchPath, pick } from '@shared/core-utils' 36import { buildVideoEmbedPath, buildVideoWatchPath, pick } from '@shared/core-utils'
37import { ffprobePromise, getAudioStream, hasAudioStream, uuidToShort } from '@shared/extra-utils' 37import { ffprobePromise, getAudioStream, hasAudioStream, uuidToShort } from '@shared/extra-utils'
38import { 38import {
@@ -103,10 +103,9 @@ import { VideoRedundancyModel } from '../redundancy/video-redundancy'
103import { ServerModel } from '../server/server' 103import { ServerModel } from '../server/server'
104import { TrackerModel } from '../server/tracker' 104import { TrackerModel } from '../server/tracker'
105import { VideoTrackerModel } from '../server/video-tracker' 105import { VideoTrackerModel } from '../server/video-tracker'
106import { setAsUpdated } from '../shared' 106import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, setAsUpdated, throwIfNotValid } from '../shared'
107import { UserModel } from '../user/user' 107import { UserModel } from '../user/user'
108import { UserVideoHistoryModel } from '../user/user-video-history' 108import { UserVideoHistoryModel } from '../user/user-video-history'
109import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'
110import { VideoViewModel } from '../view/video-view' 109import { VideoViewModel } from '../view/video-view'
111import { 110import {
112 videoFilesModelToFormattedJSON, 111 videoFilesModelToFormattedJSON,
@@ -1871,7 +1870,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1871 } 1870 }
1872 1871
1873 setAsRefreshed (transaction?: Transaction) { 1872 setAsRefreshed (transaction?: Transaction) {
1874 return setAsUpdated('video', this.id, transaction) 1873 return setAsUpdated({ sequelize: this.sequelize, table: 'video', id: this.id, transaction })
1875 } 1874 }
1876 1875
1877 // --------------------------------------------------------------------------- 1876 // ---------------------------------------------------------------------------