aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models
diff options
context:
space:
mode:
Diffstat (limited to 'server/models')
-rw-r--r--server/models/account/account-blocklist.ts10
-rw-r--r--server/models/account/account.ts10
-rw-r--r--server/models/account/user-notification.ts44
-rw-r--r--server/models/account/user.ts58
-rw-r--r--server/models/activitypub/actor.ts47
-rw-r--r--server/models/application/application.ts6
-rw-r--r--server/models/oauth/oauth-client.ts4
-rw-r--r--server/models/oauth/oauth-token.ts22
-rw-r--r--server/models/redundancy/video-redundancy.ts56
-rw-r--r--server/models/server/server-blocklist.ts8
-rw-r--r--server/models/utils.ts12
-rw-r--r--server/models/video/tag.ts2
-rw-r--r--server/models/video/thumbnail.ts4
-rw-r--r--server/models/video/video-caption.ts13
-rw-r--r--server/models/video/video-change-ownership.ts14
-rw-r--r--server/models/video/video-channel.ts16
-rw-r--r--server/models/video/video-comment.ts33
-rw-r--r--server/models/video/video-file.ts27
-rw-r--r--server/models/video/video-format-utils.ts10
-rw-r--r--server/models/video/video-import.ts8
-rw-r--r--server/models/video/video-playlist.ts47
-rw-r--r--server/models/video/video-share.ts10
-rw-r--r--server/models/video/video-streaming-playlist.ts6
-rw-r--r--server/models/video/video.ts179
24 files changed, 322 insertions, 324 deletions
diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts
index efd6ed59e..d5746ad76 100644
--- a/server/models/account/account-blocklist.ts
+++ b/server/models/account/account-blocklist.ts
@@ -8,22 +8,22 @@ enum ScopeNames {
8 WITH_ACCOUNTS = 'WITH_ACCOUNTS' 8 WITH_ACCOUNTS = 'WITH_ACCOUNTS'
9} 9}
10 10
11@Scopes({ 11@Scopes(() => ({
12 [ScopeNames.WITH_ACCOUNTS]: { 12 [ScopeNames.WITH_ACCOUNTS]: {
13 include: [ 13 include: [
14 { 14 {
15 model: () => AccountModel, 15 model: AccountModel,
16 required: true, 16 required: true,
17 as: 'ByAccount' 17 as: 'ByAccount'
18 }, 18 },
19 { 19 {
20 model: () => AccountModel, 20 model: AccountModel,
21 required: true, 21 required: true,
22 as: 'BlockedAccount' 22 as: 'BlockedAccount'
23 } 23 }
24 ] 24 ]
25 } 25 }
26}) 26}))
27 27
28@Table({ 28@Table({
29 tableName: 'accountBlocklist', 29 tableName: 'accountBlocklist',
@@ -83,7 +83,7 @@ export class AccountBlocklistModel extends Model<AccountBlocklistModel> {
83 attributes: [ 'accountId', 'id' ], 83 attributes: [ 'accountId', 'id' ],
84 where: { 84 where: {
85 accountId: { 85 accountId: {
86 [Op.any]: accountIds 86 [Op.in]: accountIds // FIXME: sequelize ANY seems broken
87 }, 87 },
88 targetAccountId 88 targetAccountId
89 }, 89 },
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index bf2ed0a61..c53312990 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -33,15 +33,15 @@ export enum ScopeNames {
33 SUMMARY = 'SUMMARY' 33 SUMMARY = 'SUMMARY'
34} 34}
35 35
36@DefaultScope({ 36@DefaultScope(() => ({
37 include: [ 37 include: [
38 { 38 {
39 model: () => ActorModel, // Default scope includes avatar and server 39 model: ActorModel, // Default scope includes avatar and server
40 required: true 40 required: true
41 } 41 }
42 ] 42 ]
43}) 43}))
44@Scopes({ 44@Scopes(() => ({
45 [ ScopeNames.SUMMARY ]: (whereActor?: WhereOptions) => { 45 [ ScopeNames.SUMMARY ]: (whereActor?: WhereOptions) => {
46 return { 46 return {
47 attributes: [ 'id', 'name' ], 47 attributes: [ 'id', 'name' ],
@@ -66,7 +66,7 @@ export enum ScopeNames {
66 ] 66 ]
67 } 67 }
68 } 68 }
69}) 69}))
70@Table({ 70@Table({
71 tableName: 'account', 71 tableName: 'account',
72 indexes: [ 72 indexes: [
diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts
index 08388f268..a4f97037b 100644
--- a/server/models/account/user-notification.ts
+++ b/server/models/account/user-notification.ts
@@ -6,7 +6,7 @@ import { isUserNotificationTypeValid } from '../../helpers/custom-validators/use
6import { UserModel } from './user' 6import { UserModel } from './user'
7import { VideoModel } from '../video/video' 7import { VideoModel } from '../video/video'
8import { VideoCommentModel } from '../video/video-comment' 8import { VideoCommentModel } from '../video/video-comment'
9import { FindOptions, Op } from 'sequelize' 9import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
10import { VideoChannelModel } from '../video/video-channel' 10import { VideoChannelModel } from '../video/video-channel'
11import { AccountModel } from './account' 11import { AccountModel } from './account'
12import { VideoAbuseModel } from '../video/video-abuse' 12import { VideoAbuseModel } from '../video/video-abuse'
@@ -24,17 +24,17 @@ enum ScopeNames {
24function buildActorWithAvatarInclude () { 24function buildActorWithAvatarInclude () {
25 return { 25 return {
26 attributes: [ 'preferredUsername' ], 26 attributes: [ 'preferredUsername' ],
27 model: () => ActorModel.unscoped(), 27 model: ActorModel.unscoped(),
28 required: true, 28 required: true,
29 include: [ 29 include: [
30 { 30 {
31 attributes: [ 'filename' ], 31 attributes: [ 'filename' ],
32 model: () => AvatarModel.unscoped(), 32 model: AvatarModel.unscoped(),
33 required: false 33 required: false
34 }, 34 },
35 { 35 {
36 attributes: [ 'host' ], 36 attributes: [ 'host' ],
37 model: () => ServerModel.unscoped(), 37 model: ServerModel.unscoped(),
38 required: false 38 required: false
39 } 39 }
40 ] 40 ]
@@ -44,7 +44,7 @@ function buildActorWithAvatarInclude () {
44function buildVideoInclude (required: boolean) { 44function buildVideoInclude (required: boolean) {
45 return { 45 return {
46 attributes: [ 'id', 'uuid', 'name' ], 46 attributes: [ 'id', 'uuid', 'name' ],
47 model: () => VideoModel.unscoped(), 47 model: VideoModel.unscoped(),
48 required 48 required
49 } 49 }
50} 50}
@@ -53,7 +53,7 @@ function buildChannelInclude (required: boolean, withActor = false) {
53 return { 53 return {
54 required, 54 required,
55 attributes: [ 'id', 'name' ], 55 attributes: [ 'id', 'name' ],
56 model: () => VideoChannelModel.unscoped(), 56 model: VideoChannelModel.unscoped(),
57 include: withActor === true ? [ buildActorWithAvatarInclude() ] : [] 57 include: withActor === true ? [ buildActorWithAvatarInclude() ] : []
58 } 58 }
59} 59}
@@ -62,12 +62,12 @@ function buildAccountInclude (required: boolean, withActor = false) {
62 return { 62 return {
63 required, 63 required,
64 attributes: [ 'id', 'name' ], 64 attributes: [ 'id', 'name' ],
65 model: () => AccountModel.unscoped(), 65 model: AccountModel.unscoped(),
66 include: withActor === true ? [ buildActorWithAvatarInclude() ] : [] 66 include: withActor === true ? [ buildActorWithAvatarInclude() ] : []
67 } 67 }
68} 68}
69 69
70@Scopes({ 70@Scopes(() => ({
71 [ScopeNames.WITH_ALL]: { 71 [ScopeNames.WITH_ALL]: {
72 include: [ 72 include: [
73 Object.assign(buildVideoInclude(false), { 73 Object.assign(buildVideoInclude(false), {
@@ -76,7 +76,7 @@ function buildAccountInclude (required: boolean, withActor = false) {
76 76
77 { 77 {
78 attributes: [ 'id', 'originCommentId' ], 78 attributes: [ 'id', 'originCommentId' ],
79 model: () => VideoCommentModel.unscoped(), 79 model: VideoCommentModel.unscoped(),
80 required: false, 80 required: false,
81 include: [ 81 include: [
82 buildAccountInclude(true, true), 82 buildAccountInclude(true, true),
@@ -86,56 +86,56 @@ function buildAccountInclude (required: boolean, withActor = false) {
86 86
87 { 87 {
88 attributes: [ 'id' ], 88 attributes: [ 'id' ],
89 model: () => VideoAbuseModel.unscoped(), 89 model: VideoAbuseModel.unscoped(),
90 required: false, 90 required: false,
91 include: [ buildVideoInclude(true) ] 91 include: [ buildVideoInclude(true) ]
92 }, 92 },
93 93
94 { 94 {
95 attributes: [ 'id' ], 95 attributes: [ 'id' ],
96 model: () => VideoBlacklistModel.unscoped(), 96 model: VideoBlacklistModel.unscoped(),
97 required: false, 97 required: false,
98 include: [ buildVideoInclude(true) ] 98 include: [ buildVideoInclude(true) ]
99 }, 99 },
100 100
101 { 101 {
102 attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ], 102 attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ],
103 model: () => VideoImportModel.unscoped(), 103 model: VideoImportModel.unscoped(),
104 required: false, 104 required: false,
105 include: [ buildVideoInclude(false) ] 105 include: [ buildVideoInclude(false) ]
106 }, 106 },
107 107
108 { 108 {
109 attributes: [ 'id', 'state' ], 109 attributes: [ 'id', 'state' ],
110 model: () => ActorFollowModel.unscoped(), 110 model: ActorFollowModel.unscoped(),
111 required: false, 111 required: false,
112 include: [ 112 include: [
113 { 113 {
114 attributes: [ 'preferredUsername' ], 114 attributes: [ 'preferredUsername' ],
115 model: () => ActorModel.unscoped(), 115 model: ActorModel.unscoped(),
116 required: true, 116 required: true,
117 as: 'ActorFollower', 117 as: 'ActorFollower',
118 include: [ 118 include: [
119 { 119 {
120 attributes: [ 'id', 'name' ], 120 attributes: [ 'id', 'name' ],
121 model: () => AccountModel.unscoped(), 121 model: AccountModel.unscoped(),
122 required: true 122 required: true
123 }, 123 },
124 { 124 {
125 attributes: [ 'filename' ], 125 attributes: [ 'filename' ],
126 model: () => AvatarModel.unscoped(), 126 model: AvatarModel.unscoped(),
127 required: false 127 required: false
128 }, 128 },
129 { 129 {
130 attributes: [ 'host' ], 130 attributes: [ 'host' ],
131 model: () => ServerModel.unscoped(), 131 model: ServerModel.unscoped(),
132 required: false 132 required: false
133 } 133 }
134 ] 134 ]
135 }, 135 },
136 { 136 {
137 attributes: [ 'preferredUsername' ], 137 attributes: [ 'preferredUsername' ],
138 model: () => ActorModel.unscoped(), 138 model: ActorModel.unscoped(),
139 required: true, 139 required: true,
140 as: 'ActorFollowing', 140 as: 'ActorFollowing',
141 include: [ 141 include: [
@@ -147,9 +147,9 @@ function buildAccountInclude (required: boolean, withActor = false) {
147 }, 147 },
148 148
149 buildAccountInclude(false, true) 149 buildAccountInclude(false, true)
150 ] as any // FIXME: sequelize typings 150 ]
151 } 151 }
152}) 152}))
153@Table({ 153@Table({
154 tableName: 'userNotification', 154 tableName: 'userNotification',
155 indexes: [ 155 indexes: [
@@ -212,7 +212,7 @@ function buildAccountInclude (required: boolean, withActor = false) {
212 } 212 }
213 } 213 }
214 } 214 }
215 ] as any // FIXME: sequelize typings 215 ] as (ModelIndexesOptions & { where?: WhereOptions })[]
216}) 216})
217export class UserNotificationModel extends Model<UserNotificationModel> { 217export class UserNotificationModel extends Model<UserNotificationModel> {
218 218
@@ -357,7 +357,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
357 where: { 357 where: {
358 userId, 358 userId,
359 id: { 359 id: {
360 [Op.any]: notificationIds 360 [Op.in]: notificationIds // FIXME: sequelize ANY seems broken
361 } 361 }
362 } 362 }
363 } 363 }
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index 8bd0397dd..4a9acd703 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -1,4 +1,4 @@
1import * as Sequelize from 'sequelize' 1import { FindOptions, literal, Op, QueryTypes } from 'sequelize'
2import { 2import {
3 AfterDestroy, 3 AfterDestroy,
4 AfterUpdate, 4 AfterUpdate,
@@ -56,33 +56,33 @@ enum ScopeNames {
56 WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL' 56 WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL'
57} 57}
58 58
59@DefaultScope({ 59@DefaultScope(() => ({
60 include: [ 60 include: [
61 { 61 {
62 model: () => AccountModel, 62 model: AccountModel,
63 required: true 63 required: true
64 }, 64 },
65 { 65 {
66 model: () => UserNotificationSettingModel, 66 model: UserNotificationSettingModel,
67 required: true 67 required: true
68 } 68 }
69 ] 69 ]
70}) 70}))
71@Scopes({ 71@Scopes(() => ({
72 [ScopeNames.WITH_VIDEO_CHANNEL]: { 72 [ScopeNames.WITH_VIDEO_CHANNEL]: {
73 include: [ 73 include: [
74 { 74 {
75 model: () => AccountModel, 75 model: AccountModel,
76 required: true, 76 required: true,
77 include: [ () => VideoChannelModel ] 77 include: [ VideoChannelModel ]
78 }, 78 },
79 { 79 {
80 model: () => UserNotificationSettingModel, 80 model: UserNotificationSettingModel,
81 required: true 81 required: true
82 } 82 }
83 ] as any // FIXME: sequelize typings 83 ]
84 } 84 }
85}) 85}))
86@Table({ 86@Table({
87 tableName: 'user', 87 tableName: 'user',
88 indexes: [ 88 indexes: [
@@ -233,26 +233,26 @@ export class UserModel extends Model<UserModel> {
233 let where = undefined 233 let where = undefined
234 if (search) { 234 if (search) {
235 where = { 235 where = {
236 [Sequelize.Op.or]: [ 236 [Op.or]: [
237 { 237 {
238 email: { 238 email: {
239 [Sequelize.Op.iLike]: '%' + search + '%' 239 [Op.iLike]: '%' + search + '%'
240 } 240 }
241 }, 241 },
242 { 242 {
243 username: { 243 username: {
244 [ Sequelize.Op.iLike ]: '%' + search + '%' 244 [ Op.iLike ]: '%' + search + '%'
245 } 245 }
246 } 246 }
247 ] 247 ]
248 } 248 }
249 } 249 }
250 250
251 const query = { 251 const query: FindOptions = {
252 attributes: { 252 attributes: {
253 include: [ 253 include: [
254 [ 254 [
255 Sequelize.literal( 255 literal(
256 '(' + 256 '(' +
257 'SELECT COALESCE(SUM("size"), 0) ' + 257 'SELECT COALESCE(SUM("size"), 0) ' +
258 'FROM (' + 258 'FROM (' +
@@ -265,7 +265,7 @@ export class UserModel extends Model<UserModel> {
265 ')' 265 ')'
266 ), 266 ),
267 'videoQuotaUsed' 267 'videoQuotaUsed'
268 ] as any // FIXME: typings 268 ]
269 ] 269 ]
270 }, 270 },
271 offset: start, 271 offset: start,
@@ -291,7 +291,7 @@ export class UserModel extends Model<UserModel> {
291 const query = { 291 const query = {
292 where: { 292 where: {
293 role: { 293 role: {
294 [Sequelize.Op.in]: roles 294 [Op.in]: roles
295 } 295 }
296 } 296 }
297 } 297 }
@@ -387,7 +387,7 @@ export class UserModel extends Model<UserModel> {
387 387
388 const query = { 388 const query = {
389 where: { 389 where: {
390 [ Sequelize.Op.or ]: [ { username }, { email } ] 390 [ Op.or ]: [ { username }, { email } ]
391 } 391 }
392 } 392 }
393 393
@@ -510,7 +510,7 @@ export class UserModel extends Model<UserModel> {
510 const query = { 510 const query = {
511 where: { 511 where: {
512 username: { 512 username: {
513 [ Sequelize.Op.like ]: `%${search}%` 513 [ Op.like ]: `%${search}%`
514 } 514 }
515 }, 515 },
516 limit: 10 516 limit: 10
@@ -591,15 +591,11 @@ export class UserModel extends Model<UserModel> {
591 591
592 const uploadedTotal = videoFile.size + totalBytes 592 const uploadedTotal = videoFile.size + totalBytes
593 const uploadedDaily = videoFile.size + totalBytesDaily 593 const uploadedDaily = videoFile.size + totalBytesDaily
594 if (this.videoQuotaDaily === -1) {
595 return uploadedTotal < this.videoQuota
596 }
597 if (this.videoQuota === -1) {
598 return uploadedDaily < this.videoQuotaDaily
599 }
600 594
601 return (uploadedTotal < this.videoQuota) && 595 if (this.videoQuotaDaily === -1) return uploadedTotal < this.videoQuota
602 (uploadedDaily < this.videoQuotaDaily) 596 if (this.videoQuota === -1) return uploadedDaily < this.videoQuotaDaily
597
598 return uploadedTotal < this.videoQuota && uploadedDaily < this.videoQuotaDaily
603 } 599 }
604 600
605 private static generateUserQuotaBaseSQL (where?: string) { 601 private static generateUserQuotaBaseSQL (where?: string) {
@@ -619,14 +615,14 @@ export class UserModel extends Model<UserModel> {
619 private static getTotalRawQuery (query: string, userId: number) { 615 private static getTotalRawQuery (query: string, userId: number) {
620 const options = { 616 const options = {
621 bind: { userId }, 617 bind: { userId },
622 type: Sequelize.QueryTypes.SELECT as Sequelize.QueryTypes.SELECT 618 type: QueryTypes.SELECT as QueryTypes.SELECT
623 } 619 }
624 620
625 return UserModel.sequelize.query<{ total: number }>(query, options) 621 return UserModel.sequelize.query<{ total: string }>(query, options)
626 .then(([ { total } ]) => { 622 .then(([ { total } ]) => {
627 if (total === null) return 0 623 if (total === null) return 0
628 624
629 return parseInt(total + '', 10) 625 return parseInt(total, 10)
630 }) 626 })
631 } 627 }
632} 628}
diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts
index 1ebee8df5..4a466441c 100644
--- a/server/models/activitypub/actor.ts
+++ b/server/models/activitypub/actor.ts
@@ -56,46 +56,46 @@ export const unusedActorAttributesForAPI = [
56 'updatedAt' 56 'updatedAt'
57] 57]
58 58
59@DefaultScope({ 59@DefaultScope(() => ({
60 include: [ 60 include: [
61 { 61 {
62 model: () => ServerModel, 62 model: ServerModel,
63 required: false 63 required: false
64 }, 64 },
65 { 65 {
66 model: () => AvatarModel, 66 model: AvatarModel,
67 required: false 67 required: false
68 } 68 }
69 ] 69 ]
70}) 70}))
71@Scopes({ 71@Scopes(() => ({
72 [ScopeNames.FULL]: { 72 [ScopeNames.FULL]: {
73 include: [ 73 include: [
74 { 74 {
75 model: () => AccountModel.unscoped(), 75 model: AccountModel.unscoped(),
76 required: false 76 required: false
77 }, 77 },
78 { 78 {
79 model: () => VideoChannelModel.unscoped(), 79 model: VideoChannelModel.unscoped(),
80 required: false, 80 required: false,
81 include: [ 81 include: [
82 { 82 {
83 model: () => AccountModel, 83 model: AccountModel,
84 required: true 84 required: true
85 } 85 }
86 ] 86 ]
87 }, 87 },
88 { 88 {
89 model: () => ServerModel, 89 model: ServerModel,
90 required: false 90 required: false
91 }, 91 },
92 { 92 {
93 model: () => AvatarModel, 93 model: AvatarModel,
94 required: false 94 required: false
95 } 95 }
96 ] as any // FIXME: sequelize typings 96 ]
97 } 97 }
98}) 98}))
99@Table({ 99@Table({
100 tableName: 'actor', 100 tableName: 'actor',
101 indexes: [ 101 indexes: [
@@ -131,7 +131,7 @@ export const unusedActorAttributesForAPI = [
131export class ActorModel extends Model<ActorModel> { 131export class ActorModel extends Model<ActorModel> {
132 132
133 @AllowNull(false) 133 @AllowNull(false)
134 @Column({ type: DataType.ENUM(...values(ACTIVITY_PUB_ACTOR_TYPES)) }) // FIXME: sequelize typings 134 @Column(DataType.ENUM(...values(ACTIVITY_PUB_ACTOR_TYPES)))
135 type: ActivityPubActorType 135 type: ActivityPubActorType
136 136
137 @AllowNull(false) 137 @AllowNull(false)
@@ -280,14 +280,16 @@ export class ActorModel extends Model<ActorModel> {
280 attributes: [ 'id' ], 280 attributes: [ 'id' ],
281 model: VideoChannelModel.unscoped(), 281 model: VideoChannelModel.unscoped(),
282 required: true, 282 required: true,
283 include: { 283 include: [
284 attributes: [ 'id' ], 284 {
285 model: VideoModel.unscoped(), 285 attributes: [ 'id' ],
286 required: true, 286 model: VideoModel.unscoped(),
287 where: { 287 required: true,
288 id: videoId 288 where: {
289 id: videoId
290 }
289 } 291 }
290 } 292 ]
291 } 293 }
292 ] 294 ]
293 } 295 }
@@ -295,7 +297,7 @@ export class ActorModel extends Model<ActorModel> {
295 transaction 297 transaction
296 } 298 }
297 299
298 return ActorModel.unscoped().findOne(query as any) // FIXME: typings 300 return ActorModel.unscoped().findOne(query)
299 } 301 }
300 302
301 static isActorUrlExist (url: string) { 303 static isActorUrlExist (url: string) {
@@ -389,8 +391,7 @@ export class ActorModel extends Model<ActorModel> {
389 } 391 }
390 392
391 static incrementFollows (id: number, column: 'followersCount' | 'followingCount', by: number) { 393 static incrementFollows (id: number, column: 'followersCount' | 'followingCount', by: number) {
392 // FIXME: typings 394 return ActorModel.increment(column, {
393 return (ActorModel as any).increment(column, {
394 by, 395 by,
395 where: { 396 where: {
396 id 397 id
diff --git a/server/models/application/application.ts b/server/models/application/application.ts
index 854a5fb36..a02208b4e 100644
--- a/server/models/application/application.ts
+++ b/server/models/application/application.ts
@@ -1,14 +1,14 @@
1import { AllowNull, Column, Default, DefaultScope, HasOne, IsInt, Model, Table } from 'sequelize-typescript' 1import { AllowNull, Column, Default, DefaultScope, HasOne, IsInt, Model, Table } from 'sequelize-typescript'
2import { AccountModel } from '../account/account' 2import { AccountModel } from '../account/account'
3 3
4@DefaultScope({ 4@DefaultScope(() => ({
5 include: [ 5 include: [
6 { 6 {
7 model: () => AccountModel, 7 model: AccountModel,
8 required: true 8 required: true
9 } 9 }
10 ] 10 ]
11}) 11}))
12@Table({ 12@Table({
13 tableName: 'application' 13 tableName: 'application'
14}) 14})
diff --git a/server/models/oauth/oauth-client.ts b/server/models/oauth/oauth-client.ts
index b4a841edd..42c59bb79 100644
--- a/server/models/oauth/oauth-client.ts
+++ b/server/models/oauth/oauth-client.ts
@@ -24,10 +24,10 @@ export class OAuthClientModel extends Model<OAuthClientModel> {
24 @Column 24 @Column
25 clientSecret: string 25 clientSecret: string
26 26
27 @Column({ type: DataType.ARRAY(DataType.STRING) }) // FIXME: sequelize typings 27 @Column(DataType.ARRAY(DataType.STRING))
28 grants: string[] 28 grants: string[]
29 29
30 @Column({ type: DataType.ARRAY(DataType.STRING) }) // FIXME: sequelize typings 30 @Column(DataType.ARRAY(DataType.STRING))
31 redirectUris: string[] 31 redirectUris: string[]
32 32
33 @CreatedAt 33 @CreatedAt
diff --git a/server/models/oauth/oauth-token.ts b/server/models/oauth/oauth-token.ts
index 3f41ee63b..903d551df 100644
--- a/server/models/oauth/oauth-token.ts
+++ b/server/models/oauth/oauth-token.ts
@@ -34,30 +34,30 @@ enum ScopeNames {
34 WITH_USER = 'WITH_USER' 34 WITH_USER = 'WITH_USER'
35} 35}
36 36
37@Scopes({ 37@Scopes(() => ({
38 [ScopeNames.WITH_USER]: { 38 [ScopeNames.WITH_USER]: {
39 include: [ 39 include: [
40 { 40 {
41 model: () => UserModel.unscoped(), 41 model: UserModel.unscoped(),
42 required: true, 42 required: true,
43 include: [ 43 include: [
44 { 44 {
45 attributes: [ 'id' ], 45 attributes: [ 'id' ],
46 model: () => AccountModel.unscoped(), 46 model: AccountModel.unscoped(),
47 required: true, 47 required: true,
48 include: [ 48 include: [
49 { 49 {
50 attributes: [ 'id', 'url' ], 50 attributes: [ 'id', 'url' ],
51 model: () => ActorModel.unscoped(), 51 model: ActorModel.unscoped(),
52 required: true 52 required: true
53 } 53 }
54 ] 54 ]
55 } 55 }
56 ] 56 ]
57 } 57 }
58 ] as any // FIXME: sequelize typings 58 ]
59 } 59 }
60}) 60}))
61@Table({ 61@Table({
62 tableName: 'oAuthToken', 62 tableName: 'oAuthToken',
63 indexes: [ 63 indexes: [
@@ -167,11 +167,13 @@ export class OAuthTokenModel extends Model<OAuthTokenModel> {
167 } 167 }
168 } 168 }
169 169
170 return OAuthTokenModel.scope(ScopeNames.WITH_USER).findOne(query).then(token => { 170 return OAuthTokenModel.scope(ScopeNames.WITH_USER)
171 if (token) token['user'] = token.User 171 .findOne(query)
172 .then(token => {
173 if (token) token[ 'user' ] = token.User
172 174
173 return token 175 return token
174 }) 176 })
175 } 177 }
176 178
177 static getByRefreshTokenAndPopulateUser (refreshToken: string) { 179 static getByRefreshTokenAndPopulateUser (refreshToken: string) {
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts
index cbeaa662b..eb2222256 100644
--- a/server/models/redundancy/video-redundancy.ts
+++ b/server/models/redundancy/video-redundancy.ts
@@ -13,7 +13,7 @@ import {
13 UpdatedAt 13 UpdatedAt
14} from 'sequelize-typescript' 14} from 'sequelize-typescript'
15import { ActorModel } from '../activitypub/actor' 15import { ActorModel } from '../activitypub/actor'
16import { getVideoSort, throwIfNotValid } from '../utils' 16import { getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils'
17import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc' 17import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc'
18import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants' 18import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants'
19import { VideoFileModel } from '../video/video-file' 19import { VideoFileModel } from '../video/video-file'
@@ -27,7 +27,7 @@ import { ServerModel } from '../server/server'
27import { sample } from 'lodash' 27import { sample } from 'lodash'
28import { isTestInstance } from '../../helpers/core-utils' 28import { isTestInstance } from '../../helpers/core-utils'
29import * as Bluebird from 'bluebird' 29import * as Bluebird from 'bluebird'
30import * as Sequelize from 'sequelize' 30import { col, FindOptions, fn, literal, Op, Transaction } from 'sequelize'
31import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist' 31import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist'
32import { CONFIG } from '../../initializers/config' 32import { CONFIG } from '../../initializers/config'
33 33
@@ -35,32 +35,32 @@ export enum ScopeNames {
35 WITH_VIDEO = 'WITH_VIDEO' 35 WITH_VIDEO = 'WITH_VIDEO'
36} 36}
37 37
38@Scopes({ 38@Scopes(() => ({
39 [ ScopeNames.WITH_VIDEO ]: { 39 [ ScopeNames.WITH_VIDEO ]: {
40 include: [ 40 include: [
41 { 41 {
42 model: () => VideoFileModel, 42 model: VideoFileModel,
43 required: false, 43 required: false,
44 include: [ 44 include: [
45 { 45 {
46 model: () => VideoModel, 46 model: VideoModel,
47 required: true 47 required: true
48 } 48 }
49 ] 49 ]
50 }, 50 },
51 { 51 {
52 model: () => VideoStreamingPlaylistModel, 52 model: VideoStreamingPlaylistModel,
53 required: false, 53 required: false,
54 include: [ 54 include: [
55 { 55 {
56 model: () => VideoModel, 56 model: VideoModel,
57 required: true 57 required: true
58 } 58 }
59 ] 59 ]
60 } 60 }
61 ] as any // FIXME: sequelize typings 61 ]
62 } 62 }
63}) 63}))
64 64
65@Table({ 65@Table({
66 tableName: 'videoRedundancy', 66 tableName: 'videoRedundancy',
@@ -192,7 +192,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
192 return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query) 192 return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query)
193 } 193 }
194 194
195 static loadByUrl (url: string, transaction?: Sequelize.Transaction) { 195 static loadByUrl (url: string, transaction?: Transaction) {
196 const query = { 196 const query = {
197 where: { 197 where: {
198 url 198 url
@@ -292,7 +292,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
292 where: { 292 where: {
293 privacy: VideoPrivacy.PUBLIC, 293 privacy: VideoPrivacy.PUBLIC,
294 views: { 294 views: {
295 [ Sequelize.Op.gte ]: minViews 295 [ Op.gte ]: minViews
296 } 296 }
297 }, 297 },
298 include: [ 298 include: [
@@ -315,7 +315,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
315 actorId: actor.id, 315 actorId: actor.id,
316 strategy, 316 strategy,
317 createdAt: { 317 createdAt: {
318 [ Sequelize.Op.lt ]: expiredDate 318 [ Op.lt ]: expiredDate
319 } 319 }
320 } 320 }
321 } 321 }
@@ -326,7 +326,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
326 static async getTotalDuplicated (strategy: VideoRedundancyStrategy) { 326 static async getTotalDuplicated (strategy: VideoRedundancyStrategy) {
327 const actor = await getServerActor() 327 const actor = await getServerActor()
328 328
329 const options = { 329 const query: FindOptions = {
330 include: [ 330 include: [
331 { 331 {
332 attributes: [], 332 attributes: [],
@@ -340,12 +340,8 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
340 ] 340 ]
341 } 341 }
342 342
343 return VideoFileModel.sum('size', options as any) // FIXME: typings 343 return VideoFileModel.aggregate('size', 'SUM', query)
344 .then(v => { 344 .then(result => parseAggregateResult(result))
345 if (!v || isNaN(v)) return 0
346
347 return v
348 })
349 } 345 }
350 346
351 static async listLocalExpired () { 347 static async listLocalExpired () {
@@ -355,7 +351,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
355 where: { 351 where: {
356 actorId: actor.id, 352 actorId: actor.id,
357 expiresOn: { 353 expiresOn: {
358 [ Sequelize.Op.lt ]: new Date() 354 [ Op.lt ]: new Date()
359 } 355 }
360 } 356 }
361 } 357 }
@@ -369,10 +365,10 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
369 const query = { 365 const query = {
370 where: { 366 where: {
371 actorId: { 367 actorId: {
372 [Sequelize.Op.ne]: actor.id 368 [Op.ne]: actor.id
373 }, 369 },
374 expiresOn: { 370 expiresOn: {
375 [ Sequelize.Op.lt ]: new Date() 371 [ Op.lt ]: new Date()
376 } 372 }
377 } 373 }
378 } 374 }
@@ -428,12 +424,12 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
428 static async getStats (strategy: VideoRedundancyStrategy) { 424 static async getStats (strategy: VideoRedundancyStrategy) {
429 const actor = await getServerActor() 425 const actor = await getServerActor()
430 426
431 const query = { 427 const query: FindOptions = {
432 raw: true, 428 raw: true,
433 attributes: [ 429 attributes: [
434 [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoFile.size')), '0'), 'totalUsed' ], 430 [ fn('COALESCE', fn('SUM', col('VideoFile.size')), '0'), 'totalUsed' ],
435 [ Sequelize.fn('COUNT', Sequelize.fn('DISTINCT', Sequelize.col('videoId'))), 'totalVideos' ], 431 [ fn('COUNT', fn('DISTINCT', col('videoId'))), 'totalVideos' ],
436 [ Sequelize.fn('COUNT', Sequelize.col('videoFileId')), 'totalVideoFiles' ] 432 [ fn('COUNT', col('videoFileId')), 'totalVideoFiles' ]
437 ], 433 ],
438 where: { 434 where: {
439 strategy, 435 strategy,
@@ -448,9 +444,9 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
448 ] 444 ]
449 } 445 }
450 446
451 return VideoRedundancyModel.findOne(query as any) // FIXME: typings 447 return VideoRedundancyModel.findOne(query)
452 .then((r: any) => ({ 448 .then((r: any) => ({
453 totalUsed: parseInt(r.totalUsed.toString(), 10), 449 totalUsed: parseAggregateResult(r.totalUsed),
454 totalVideos: r.totalVideos, 450 totalVideos: r.totalVideos,
455 totalVideoFiles: r.totalVideoFiles 451 totalVideoFiles: r.totalVideoFiles
456 })) 452 }))
@@ -503,7 +499,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
503 private static async buildVideoFileForDuplication () { 499 private static async buildVideoFileForDuplication () {
504 const actor = await getServerActor() 500 const actor = await getServerActor()
505 501
506 const notIn = Sequelize.literal( 502 const notIn = literal(
507 '(' + 503 '(' +
508 `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id} AND "videoFileId" IS NOT NULL` + 504 `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id} AND "videoFileId" IS NOT NULL` +
509 ')' 505 ')'
@@ -515,7 +511,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
515 required: true, 511 required: true,
516 where: { 512 where: {
517 id: { 513 id: {
518 [ Sequelize.Op.notIn ]: notIn 514 [ Op.notIn ]: notIn
519 } 515 }
520 } 516 }
521 } 517 }
diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts
index 450f27152..92c01f642 100644
--- a/server/models/server/server-blocklist.ts
+++ b/server/models/server/server-blocklist.ts
@@ -9,11 +9,11 @@ enum ScopeNames {
9 WITH_SERVER = 'WITH_SERVER' 9 WITH_SERVER = 'WITH_SERVER'
10} 10}
11 11
12@Scopes({ 12@Scopes(() => ({
13 [ScopeNames.WITH_ACCOUNT]: { 13 [ScopeNames.WITH_ACCOUNT]: {
14 include: [ 14 include: [
15 { 15 {
16 model: () => AccountModel, 16 model: AccountModel,
17 required: true 17 required: true
18 } 18 }
19 ] 19 ]
@@ -21,12 +21,12 @@ enum ScopeNames {
21 [ScopeNames.WITH_SERVER]: { 21 [ScopeNames.WITH_SERVER]: {
22 include: [ 22 include: [
23 { 23 {
24 model: () => ServerModel, 24 model: ServerModel,
25 required: true 25 required: true
26 } 26 }
27 ] 27 ]
28 } 28 }
29}) 29}))
30 30
31@Table({ 31@Table({
32 tableName: 'serverBlocklist', 32 tableName: 'serverBlocklist',
diff --git a/server/models/utils.ts b/server/models/utils.ts
index 98170a00e..2b172f608 100644
--- a/server/models/utils.ts
+++ b/server/models/utils.ts
@@ -118,6 +118,15 @@ function buildWhereIdOrUUID (id: number | string) {
118 return validator.isInt('' + id) ? { id } : { uuid: id } 118 return validator.isInt('' + id) ? { id } : { uuid: id }
119} 119}
120 120
121function parseAggregateResult (result: any) {
122 if (!result) return 0
123
124 const total = parseInt(result + '', 10)
125 if (isNaN(total)) return 0
126
127 return total
128}
129
121// --------------------------------------------------------------------------- 130// ---------------------------------------------------------------------------
122 131
123export { 132export {
@@ -131,7 +140,8 @@ export {
131 buildServerIdsFollowedBy, 140 buildServerIdsFollowedBy,
132 buildTrigramSearchIndex, 141 buildTrigramSearchIndex,
133 buildWhereIdOrUUID, 142 buildWhereIdOrUUID,
134 isOutdated 143 isOutdated,
144 parseAggregateResult
135} 145}
136 146
137// --------------------------------------------------------------------------- 147// ---------------------------------------------------------------------------
diff --git a/server/models/video/tag.ts b/server/models/video/tag.ts
index 048b47613..0fc3cfd4c 100644
--- a/server/models/video/tag.ts
+++ b/server/models/video/tag.ts
@@ -75,7 +75,7 @@ export class TagModel extends Model<TagModel> {
75 type: QueryTypes.SELECT as QueryTypes.SELECT 75 type: QueryTypes.SELECT as QueryTypes.SELECT
76 } 76 }
77 77
78 return TagModel.sequelize.query<{ name }>(query, options) 78 return TagModel.sequelize.query<{ name: string }>(query, options)
79 .then(data => data.map(d => d.name)) 79 .then(data => data.map(d => d.name))
80 } 80 }
81} 81}
diff --git a/server/models/video/thumbnail.ts b/server/models/video/thumbnail.ts
index baa5533ac..ec945893f 100644
--- a/server/models/video/thumbnail.ts
+++ b/server/models/video/thumbnail.ts
@@ -75,8 +75,8 @@ export class ThumbnailModel extends Model<ThumbnailModel> {
75 updatedAt: Date 75 updatedAt: Date
76 76
77 private static types: { [ id in ThumbnailType ]: { label: string, directory: string, staticPath: string } } = { 77 private static types: { [ id in ThumbnailType ]: { label: string, directory: string, staticPath: string } } = {
78 [ThumbnailType.THUMBNAIL]: { 78 [ThumbnailType.MINIATURE]: {
79 label: 'thumbnail', 79 label: 'miniature',
80 directory: CONFIG.STORAGE.THUMBNAILS_DIR, 80 directory: CONFIG.STORAGE.THUMBNAILS_DIR,
81 staticPath: STATIC_PATHS.THUMBNAILS 81 staticPath: STATIC_PATHS.THUMBNAILS
82 }, 82 },
diff --git a/server/models/video/video-caption.ts b/server/models/video/video-caption.ts
index 45c60e26b..76243bf48 100644
--- a/server/models/video/video-caption.ts
+++ b/server/models/video/video-caption.ts
@@ -12,7 +12,7 @@ import {
12 Table, 12 Table,
13 UpdatedAt 13 UpdatedAt
14} from 'sequelize-typescript' 14} from 'sequelize-typescript'
15import { throwIfNotValid } from '../utils' 15import { buildWhereIdOrUUID, throwIfNotValid } from '../utils'
16import { VideoModel } from './video' 16import { VideoModel } from './video'
17import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions' 17import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions'
18import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model' 18import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model'
@@ -26,17 +26,17 @@ export enum ScopeNames {
26 WITH_VIDEO_UUID_AND_REMOTE = 'WITH_VIDEO_UUID_AND_REMOTE' 26 WITH_VIDEO_UUID_AND_REMOTE = 'WITH_VIDEO_UUID_AND_REMOTE'
27} 27}
28 28
29@Scopes({ 29@Scopes(() => ({
30 [ScopeNames.WITH_VIDEO_UUID_AND_REMOTE]: { 30 [ScopeNames.WITH_VIDEO_UUID_AND_REMOTE]: {
31 include: [ 31 include: [
32 { 32 {
33 attributes: [ 'uuid', 'remote' ], 33 attributes: [ 'uuid', 'remote' ],
34 model: () => VideoModel.unscoped(), 34 model: VideoModel.unscoped(),
35 required: true 35 required: true
36 } 36 }
37 ] 37 ]
38 } 38 }
39}) 39}))
40 40
41@Table({ 41@Table({
42 tableName: 'videoCaption', 42 tableName: 'videoCaption',
@@ -97,12 +97,9 @@ export class VideoCaptionModel extends Model<VideoCaptionModel> {
97 const videoInclude = { 97 const videoInclude = {
98 model: VideoModel.unscoped(), 98 model: VideoModel.unscoped(),
99 attributes: [ 'id', 'remote', 'uuid' ], 99 attributes: [ 'id', 'remote', 'uuid' ],
100 where: { } 100 where: buildWhereIdOrUUID(videoId)
101 } 101 }
102 102
103 if (typeof videoId === 'string') videoInclude.where['uuid'] = videoId
104 else videoInclude.where['id'] = videoId
105
106 const query = { 103 const query = {
107 where: { 104 where: {
108 language 105 language
diff --git a/server/models/video/video-change-ownership.ts b/server/models/video/video-change-ownership.ts
index a4f4d53f1..171d4574d 100644
--- a/server/models/video/video-change-ownership.ts
+++ b/server/models/video/video-change-ownership.ts
@@ -23,29 +23,29 @@ enum ScopeNames {
23 } 23 }
24 ] 24 ]
25}) 25})
26@Scopes({ 26@Scopes(() => ({
27 [ScopeNames.FULL]: { 27 [ScopeNames.FULL]: {
28 include: [ 28 include: [
29 { 29 {
30 model: () => AccountModel, 30 model: AccountModel,
31 as: 'Initiator', 31 as: 'Initiator',
32 required: true 32 required: true
33 }, 33 },
34 { 34 {
35 model: () => AccountModel, 35 model: AccountModel,
36 as: 'NextOwner', 36 as: 'NextOwner',
37 required: true 37 required: true
38 }, 38 },
39 { 39 {
40 model: () => VideoModel, 40 model: VideoModel,
41 required: true, 41 required: true,
42 include: [ 42 include: [
43 { model: () => VideoFileModel } 43 { model: VideoFileModel }
44 ] 44 ]
45 } 45 }
46 ] as any // FIXME: sequelize typings 46 ]
47 } 47 }
48}) 48}))
49export class VideoChangeOwnershipModel extends Model<VideoChangeOwnershipModel> { 49export class VideoChangeOwnershipModel extends Model<VideoChangeOwnershipModel> {
50 @CreatedAt 50 @CreatedAt
51 createdAt: Date 51 createdAt: Date
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index 901006dea..fb70e6625 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -58,15 +58,15 @@ type AvailableForListOptions = {
58 actorId: number 58 actorId: number
59} 59}
60 60
61@DefaultScope({ 61@DefaultScope(() => ({
62 include: [ 62 include: [
63 { 63 {
64 model: () => ActorModel, 64 model: ActorModel,
65 required: true 65 required: true
66 } 66 }
67 ] 67 ]
68}) 68}))
69@Scopes({ 69@Scopes(() => ({
70 [ScopeNames.SUMMARY]: (withAccount = false) => { 70 [ScopeNames.SUMMARY]: (withAccount = false) => {
71 const base: FindOptions = { 71 const base: FindOptions = {
72 attributes: [ 'name', 'description', 'id', 'actorId' ], 72 attributes: [ 'name', 'description', 'id', 'actorId' ],
@@ -142,22 +142,22 @@ type AvailableForListOptions = {
142 [ScopeNames.WITH_ACCOUNT]: { 142 [ScopeNames.WITH_ACCOUNT]: {
143 include: [ 143 include: [
144 { 144 {
145 model: () => AccountModel, 145 model: AccountModel,
146 required: true 146 required: true
147 } 147 }
148 ] 148 ]
149 }, 149 },
150 [ScopeNames.WITH_VIDEOS]: { 150 [ScopeNames.WITH_VIDEOS]: {
151 include: [ 151 include: [
152 () => VideoModel 152 VideoModel
153 ] 153 ]
154 }, 154 },
155 [ScopeNames.WITH_ACTOR]: { 155 [ScopeNames.WITH_ACTOR]: {
156 include: [ 156 include: [
157 () => ActorModel 157 ActorModel
158 ] 158 ]
159 } 159 }
160}) 160}))
161@Table({ 161@Table({
162 tableName: 'videoChannel', 162 tableName: 'videoChannel',
163 indexes 163 indexes
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts
index 5f7cd3671..fee11ec5f 100644
--- a/server/models/video/video-comment.ts
+++ b/server/models/video/video-comment.ts
@@ -30,7 +30,7 @@ import { UserModel } from '../account/user'
30import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor' 30import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor'
31import { regexpCapture } from '../../helpers/regexp' 31import { regexpCapture } from '../../helpers/regexp'
32import { uniq } from 'lodash' 32import { uniq } from 'lodash'
33import { FindOptions, Op, Order, Sequelize, Transaction } from 'sequelize' 33import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize'
34 34
35enum ScopeNames { 35enum ScopeNames {
36 WITH_ACCOUNT = 'WITH_ACCOUNT', 36 WITH_ACCOUNT = 'WITH_ACCOUNT',
@@ -39,7 +39,7 @@ enum ScopeNames {
39 ATTRIBUTES_FOR_API = 'ATTRIBUTES_FOR_API' 39 ATTRIBUTES_FOR_API = 'ATTRIBUTES_FOR_API'
40} 40}
41 41
42@Scopes({ 42@Scopes(() => ({
43 [ScopeNames.ATTRIBUTES_FOR_API]: (serverAccountId: number, userAccountId?: number) => { 43 [ScopeNames.ATTRIBUTES_FOR_API]: (serverAccountId: number, userAccountId?: number) => {
44 return { 44 return {
45 attributes: { 45 attributes: {
@@ -63,34 +63,34 @@ enum ScopeNames {
63 ] 63 ]
64 ] 64 ]
65 } 65 }
66 } 66 } as FindOptions
67 }, 67 },
68 [ScopeNames.WITH_ACCOUNT]: { 68 [ScopeNames.WITH_ACCOUNT]: {
69 include: [ 69 include: [
70 { 70 {
71 model: () => AccountModel, 71 model: AccountModel,
72 include: [ 72 include: [
73 { 73 {
74 model: () => ActorModel, 74 model: ActorModel,
75 include: [ 75 include: [
76 { 76 {
77 model: () => ServerModel, 77 model: ServerModel,
78 required: false 78 required: false
79 }, 79 },
80 { 80 {
81 model: () => AvatarModel, 81 model: AvatarModel,
82 required: false 82 required: false
83 } 83 }
84 ] 84 ]
85 } 85 }
86 ] 86 ]
87 } 87 }
88 ] as any // FIXME: sequelize typings 88 ]
89 }, 89 },
90 [ScopeNames.WITH_IN_REPLY_TO]: { 90 [ScopeNames.WITH_IN_REPLY_TO]: {
91 include: [ 91 include: [
92 { 92 {
93 model: () => VideoCommentModel, 93 model: VideoCommentModel,
94 as: 'InReplyToVideoComment' 94 as: 'InReplyToVideoComment'
95 } 95 }
96 ] 96 ]
@@ -98,19 +98,19 @@ enum ScopeNames {
98 [ScopeNames.WITH_VIDEO]: { 98 [ScopeNames.WITH_VIDEO]: {
99 include: [ 99 include: [
100 { 100 {
101 model: () => VideoModel, 101 model: VideoModel,
102 required: true, 102 required: true,
103 include: [ 103 include: [
104 { 104 {
105 model: () => VideoChannelModel.unscoped(), 105 model: VideoChannelModel.unscoped(),
106 required: true, 106 required: true,
107 include: [ 107 include: [
108 { 108 {
109 model: () => AccountModel, 109 model: AccountModel,
110 required: true, 110 required: true,
111 include: [ 111 include: [
112 { 112 {
113 model: () => ActorModel, 113 model: ActorModel,
114 required: true 114 required: true
115 } 115 }
116 ] 116 ]
@@ -119,9 +119,9 @@ enum ScopeNames {
119 } 119 }
120 ] 120 ]
121 } 121 }
122 ] as any // FIXME: sequelize typings 122 ]
123 } 123 }
124}) 124}))
125@Table({ 125@Table({
126 tableName: 'videoComment', 126 tableName: 'videoComment',
127 indexes: [ 127 indexes: [
@@ -313,8 +313,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
313 } 313 }
314 } 314 }
315 315
316 // FIXME: typings 316 const scopes: (string | ScopeOptions)[] = [
317 const scopes: any[] = [
318 ScopeNames.WITH_ACCOUNT, 317 ScopeNames.WITH_ACCOUNT,
319 { 318 {
320 method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ] 319 method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ]
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts
index c14d96bc5..2203a7aba 100644
--- a/server/models/video/video-file.ts
+++ b/server/models/video/video-file.ts
@@ -19,11 +19,11 @@ import {
19 isVideoFileSizeValid, 19 isVideoFileSizeValid,
20 isVideoFPSResolutionValid 20 isVideoFPSResolutionValid
21} from '../../helpers/custom-validators/videos' 21} from '../../helpers/custom-validators/videos'
22import { throwIfNotValid } from '../utils' 22import { parseAggregateResult, throwIfNotValid } from '../utils'
23import { VideoModel } from './video' 23import { VideoModel } from './video'
24import * as Sequelize from 'sequelize'
25import { VideoRedundancyModel } from '../redundancy/video-redundancy' 24import { VideoRedundancyModel } from '../redundancy/video-redundancy'
26import { VideoStreamingPlaylistModel } from './video-streaming-playlist' 25import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
26import { FindOptions, QueryTypes, Transaction } from 'sequelize'
27 27
28@Table({ 28@Table({
29 tableName: 'videoFile', 29 tableName: 'videoFile',
@@ -97,15 +97,13 @@ export class VideoFileModel extends Model<VideoFileModel> {
97 static doesInfohashExist (infoHash: string) { 97 static doesInfohashExist (infoHash: string) {
98 const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1' 98 const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1'
99 const options = { 99 const options = {
100 type: Sequelize.QueryTypes.SELECT, 100 type: QueryTypes.SELECT,
101 bind: { infoHash }, 101 bind: { infoHash },
102 raw: true 102 raw: true
103 } 103 }
104 104
105 return VideoModel.sequelize.query(query, options) 105 return VideoModel.sequelize.query(query, options)
106 .then(results => { 106 .then(results => results.length === 1)
107 return results.length === 1
108 })
109 } 107 }
110 108
111 static loadWithVideo (id: number) { 109 static loadWithVideo (id: number) {
@@ -121,7 +119,7 @@ export class VideoFileModel extends Model<VideoFileModel> {
121 return VideoFileModel.findByPk(id, options) 119 return VideoFileModel.findByPk(id, options)
122 } 120 }
123 121
124 static listByStreamingPlaylist (streamingPlaylistId: number, transaction: Sequelize.Transaction) { 122 static listByStreamingPlaylist (streamingPlaylistId: number, transaction: Transaction) {
125 const query = { 123 const query = {
126 include: [ 124 include: [
127 { 125 {
@@ -144,8 +142,8 @@ export class VideoFileModel extends Model<VideoFileModel> {
144 return VideoFileModel.findAll(query) 142 return VideoFileModel.findAll(query)
145 } 143 }
146 144
147 static async getStats () { 145 static getStats () {
148 let totalLocalVideoFilesSize = await VideoFileModel.sum('size', { 146 const query: FindOptions = {
149 include: [ 147 include: [
150 { 148 {
151 attributes: [], 149 attributes: [],
@@ -155,13 +153,12 @@ export class VideoFileModel extends Model<VideoFileModel> {
155 } 153 }
156 } 154 }
157 ] 155 ]
158 } as any)
159 // Sequelize could return null...
160 if (!totalLocalVideoFilesSize) totalLocalVideoFilesSize = 0
161
162 return {
163 totalLocalVideoFilesSize
164 } 156 }
157
158 return VideoFileModel.aggregate('size', 'SUM', query)
159 .then(result => ({
160 totalLocalVideoFilesSize: parseAggregateResult(result)
161 }))
165 } 162 }
166 163
167 hasSameUniqueKeysThan (other: VideoFileModel) { 164 hasSameUniqueKeysThan (other: VideoFileModel) {
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts
index 89992a5a8..877fcbc57 100644
--- a/server/models/video/video-format-utils.ts
+++ b/server/models/video/video-format-utils.ts
@@ -59,7 +59,7 @@ function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormatting
59 views: video.views, 59 views: video.views,
60 likes: video.likes, 60 likes: video.likes,
61 dislikes: video.dislikes, 61 dislikes: video.dislikes,
62 thumbnailPath: video.getThumbnailStaticPath(), 62 thumbnailPath: video.getMiniatureStaticPath(),
63 previewPath: video.getPreviewStaticPath(), 63 previewPath: video.getPreviewStaticPath(),
64 embedPath: video.getEmbedStaticPath(), 64 embedPath: video.getEmbedStaticPath(),
65 createdAt: video.createdAt, 65 createdAt: video.createdAt,
@@ -301,6 +301,8 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject {
301 }) 301 })
302 } 302 }
303 303
304 const miniature = video.getMiniature()
305
304 return { 306 return {
305 type: 'Video' as 'Video', 307 type: 'Video' as 'Video',
306 id: video.url, 308 id: video.url,
@@ -326,10 +328,10 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject {
326 subtitleLanguage, 328 subtitleLanguage,
327 icon: { 329 icon: {
328 type: 'Image', 330 type: 'Image',
329 url: video.getThumbnail().getUrl(), 331 url: miniature.getUrl(),
330 mediaType: 'image/jpeg', 332 mediaType: 'image/jpeg',
331 width: video.getThumbnail().width, 333 width: miniature.width,
332 height: video.getThumbnail().height 334 height: miniature.height
333 }, 335 },
334 url, 336 url,
335 likes: getVideoLikesActivityPubUrl(video), 337 likes: getVideoLikesActivityPubUrl(video),
diff --git a/server/models/video/video-import.ts b/server/models/video/video-import.ts
index 588a13a4f..480a671c8 100644
--- a/server/models/video/video-import.ts
+++ b/server/models/video/video-import.ts
@@ -21,18 +21,18 @@ import { VideoImport, VideoImportState } from '../../../shared'
21import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos' 21import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos'
22import { UserModel } from '../account/user' 22import { UserModel } from '../account/user'
23 23
24@DefaultScope({ 24@DefaultScope(() => ({
25 include: [ 25 include: [
26 { 26 {
27 model: () => UserModel.unscoped(), 27 model: UserModel.unscoped(),
28 required: true 28 required: true
29 }, 29 },
30 { 30 {
31 model: () => VideoModel.scope([ VideoModelScopeNames.WITH_ACCOUNT_DETAILS, VideoModelScopeNames.WITH_TAGS]), 31 model: VideoModel.scope([ VideoModelScopeNames.WITH_ACCOUNT_DETAILS, VideoModelScopeNames.WITH_TAGS]),
32 required: false 32 required: false
33 } 33 }
34 ] 34 ]
35}) 35}))
36 36
37@Table({ 37@Table({
38 tableName: 'videoImport', 38 tableName: 'videoImport',
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts
index 3e436acfc..63b4a0715 100644
--- a/server/models/video/video-playlist.ts
+++ b/server/models/video/video-playlist.ts
@@ -42,7 +42,7 @@ import { activityPubCollectionPagination } from '../../helpers/activitypub'
42import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model' 42import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model'
43import { ThumbnailModel } from './thumbnail' 43import { ThumbnailModel } from './thumbnail'
44import { ActivityIconObject } from '../../../shared/models/activitypub/objects' 44import { ActivityIconObject } from '../../../shared/models/activitypub/objects'
45import { fn, literal, Op, Transaction } from 'sequelize' 45import { FindOptions, literal, Op, ScopeOptions, Transaction, WhereOptions } from 'sequelize'
46 46
47enum ScopeNames { 47enum ScopeNames {
48 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', 48 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
@@ -61,11 +61,11 @@ type AvailableForListOptions = {
61 privateAndUnlisted?: boolean 61 privateAndUnlisted?: boolean
62} 62}
63 63
64@Scopes({ 64@Scopes(() => ({
65 [ ScopeNames.WITH_THUMBNAIL ]: { 65 [ ScopeNames.WITH_THUMBNAIL ]: {
66 include: [ 66 include: [
67 { 67 {
68 model: () => ThumbnailModel, 68 model: ThumbnailModel,
69 required: false 69 required: false
70 } 70 }
71 ] 71 ]
@@ -74,20 +74,16 @@ type AvailableForListOptions = {
74 attributes: { 74 attributes: {
75 include: [ 75 include: [
76 [ 76 [
77 fn('COUNT', 'toto'),
78 'coucou'
79 ],
80 [
81 literal('(SELECT COUNT("id") FROM "videoPlaylistElement" WHERE "videoPlaylistId" = "VideoPlaylistModel"."id")'), 77 literal('(SELECT COUNT("id") FROM "videoPlaylistElement" WHERE "videoPlaylistId" = "VideoPlaylistModel"."id")'),
82 'videosLength' 78 'videosLength'
83 ] 79 ]
84 ] 80 ]
85 } 81 }
86 }, 82 } as FindOptions,
87 [ ScopeNames.WITH_ACCOUNT ]: { 83 [ ScopeNames.WITH_ACCOUNT ]: {
88 include: [ 84 include: [
89 { 85 {
90 model: () => AccountModel, 86 model: AccountModel,
91 required: true 87 required: true
92 } 88 }
93 ] 89 ]
@@ -95,11 +91,11 @@ type AvailableForListOptions = {
95 [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY ]: { 91 [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY ]: {
96 include: [ 92 include: [
97 { 93 {
98 model: () => AccountModel.scope(AccountScopeNames.SUMMARY), 94 model: AccountModel.scope(AccountScopeNames.SUMMARY),
99 required: true 95 required: true
100 }, 96 },
101 { 97 {
102 model: () => VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY), 98 model: VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY),
103 required: false 99 required: false
104 } 100 }
105 ] 101 ]
@@ -107,11 +103,11 @@ type AvailableForListOptions = {
107 [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL ]: { 103 [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL ]: {
108 include: [ 104 include: [
109 { 105 {
110 model: () => AccountModel, 106 model: AccountModel,
111 required: true 107 required: true
112 }, 108 },
113 { 109 {
114 model: () => VideoChannelModel, 110 model: VideoChannelModel,
115 required: false 111 required: false
116 } 112 }
117 ] 113 ]
@@ -132,7 +128,7 @@ type AvailableForListOptions = {
132 ] 128 ]
133 } 129 }
134 130
135 const whereAnd: any[] = [] 131 const whereAnd: WhereOptions[] = []
136 132
137 if (options.privateAndUnlisted !== true) { 133 if (options.privateAndUnlisted !== true) {
138 whereAnd.push({ 134 whereAnd.push({
@@ -178,9 +174,9 @@ type AvailableForListOptions = {
178 required: false 174 required: false
179 } 175 }
180 ] 176 ]
181 } 177 } as FindOptions
182 } 178 }
183}) 179}))
184 180
185@Table({ 181@Table({
186 tableName: 'videoPlaylist', 182 tableName: 'videoPlaylist',
@@ -269,6 +265,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
269 VideoPlaylistElements: VideoPlaylistElementModel[] 265 VideoPlaylistElements: VideoPlaylistElementModel[]
270 266
271 @HasOne(() => ThumbnailModel, { 267 @HasOne(() => ThumbnailModel, {
268
272 foreignKey: { 269 foreignKey: {
273 name: 'videoPlaylistId', 270 name: 'videoPlaylistId',
274 allowNull: true 271 allowNull: true
@@ -294,7 +291,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
294 order: getSort(options.sort) 291 order: getSort(options.sort)
295 } 292 }
296 293
297 const scopes = [ 294 const scopes: (string | ScopeOptions)[] = [
298 { 295 {
299 method: [ 296 method: [
300 ScopeNames.AVAILABLE_FOR_LIST, 297 ScopeNames.AVAILABLE_FOR_LIST,
@@ -306,7 +303,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
306 privateAndUnlisted: options.privateAndUnlisted 303 privateAndUnlisted: options.privateAndUnlisted
307 } as AvailableForListOptions 304 } as AvailableForListOptions
308 ] 305 ]
309 } as any, // FIXME: typings 306 },
310 ScopeNames.WITH_VIDEOS_LENGTH, 307 ScopeNames.WITH_VIDEOS_LENGTH,
311 ScopeNames.WITH_THUMBNAIL 308 ScopeNames.WITH_THUMBNAIL
312 ] 309 ]
@@ -348,7 +345,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
348 model: VideoPlaylistElementModel.unscoped(), 345 model: VideoPlaylistElementModel.unscoped(),
349 where: { 346 where: {
350 videoId: { 347 videoId: {
351 [Op.any]: videoIds 348 [Op.in]: videoIds // FIXME: sequelize ANY seems broken
352 } 349 }
353 }, 350 },
354 required: true 351 required: true
@@ -427,12 +424,10 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
427 return VideoPlaylistModel.update({ privacy: VideoPlaylistPrivacy.PRIVATE, videoChannelId: null }, query) 424 return VideoPlaylistModel.update({ privacy: VideoPlaylistPrivacy.PRIVATE, videoChannelId: null }, query)
428 } 425 }
429 426
430 setThumbnail (thumbnail: ThumbnailModel) { 427 async setAndSaveThumbnail (thumbnail: ThumbnailModel, t: Transaction) {
431 this.Thumbnail = thumbnail 428 thumbnail.videoPlaylistId = this.id
432 }
433 429
434 getThumbnail () { 430 this.Thumbnail = await thumbnail.save({ transaction: t })
435 return this.Thumbnail
436 } 431 }
437 432
438 hasThumbnail () { 433 hasThumbnail () {
@@ -448,13 +443,13 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
448 getThumbnailUrl () { 443 getThumbnailUrl () {
449 if (!this.hasThumbnail()) return null 444 if (!this.hasThumbnail()) return null
450 445
451 return WEBSERVER.URL + STATIC_PATHS.THUMBNAILS + this.getThumbnail().filename 446 return WEBSERVER.URL + STATIC_PATHS.THUMBNAILS + this.Thumbnail.filename
452 } 447 }
453 448
454 getThumbnailStaticPath () { 449 getThumbnailStaticPath () {
455 if (!this.hasThumbnail()) return null 450 if (!this.hasThumbnail()) return null
456 451
457 return join(STATIC_PATHS.THUMBNAILS, this.getThumbnail().filename) 452 return join(STATIC_PATHS.THUMBNAILS, this.Thumbnail.filename)
458 } 453 }
459 454
460 setAsRefreshed () { 455 setAsRefreshed () {
diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts
index c83f6c5b0..fda2d7cea 100644
--- a/server/models/video/video-share.ts
+++ b/server/models/video/video-share.ts
@@ -14,15 +14,15 @@ enum ScopeNames {
14 WITH_ACTOR = 'WITH_ACTOR' 14 WITH_ACTOR = 'WITH_ACTOR'
15} 15}
16 16
17@Scopes({ 17@Scopes(() => ({
18 [ScopeNames.FULL]: { 18 [ScopeNames.FULL]: {
19 include: [ 19 include: [
20 { 20 {
21 model: () => ActorModel, 21 model: ActorModel,
22 required: true 22 required: true
23 }, 23 },
24 { 24 {
25 model: () => VideoModel, 25 model: VideoModel,
26 required: true 26 required: true
27 } 27 }
28 ] 28 ]
@@ -30,12 +30,12 @@ enum ScopeNames {
30 [ScopeNames.WITH_ACTOR]: { 30 [ScopeNames.WITH_ACTOR]: {
31 include: [ 31 include: [
32 { 32 {
33 model: () => ActorModel, 33 model: ActorModel,
34 required: true 34 required: true
35 } 35 }
36 ] 36 ]
37 } 37 }
38}) 38}))
39@Table({ 39@Table({
40 tableName: 'videoShare', 40 tableName: 'videoShare',
41 indexes: [ 41 indexes: [
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts
index b30267e09..31dc82c54 100644
--- a/server/models/video/video-streaming-playlist.ts
+++ b/server/models/video/video-streaming-playlist.ts
@@ -26,7 +26,7 @@ import { QueryTypes, Op } from 'sequelize'
26 fields: [ 'p2pMediaLoaderInfohashes' ], 26 fields: [ 'p2pMediaLoaderInfohashes' ],
27 using: 'gin' 27 using: 'gin'
28 } 28 }
29 ] as any // FIXME: sequelize typings 29 ]
30}) 30})
31export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistModel> { 31export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistModel> {
32 @CreatedAt 32 @CreatedAt
@@ -46,7 +46,7 @@ export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistMod
46 46
47 @AllowNull(false) 47 @AllowNull(false)
48 @Is('VideoStreamingPlaylistInfoHashes', value => throwIfNotValid(value, v => isArrayOf(v, isVideoFileInfoHashValid), 'info hashes')) 48 @Is('VideoStreamingPlaylistInfoHashes', value => throwIfNotValid(value, v => isArrayOf(v, isVideoFileInfoHashValid), 'info hashes'))
49 @Column({ type: DataType.ARRAY(DataType.STRING) }) // FIXME: typings 49 @Column(DataType.ARRAY(DataType.STRING))
50 p2pMediaLoaderInfohashes: string[] 50 p2pMediaLoaderInfohashes: string[]
51 51
52 @AllowNull(false) 52 @AllowNull(false)
@@ -87,7 +87,7 @@ export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistMod
87 raw: true 87 raw: true
88 } 88 }
89 89
90 return VideoModel.sequelize.query<any>(query, options) 90 return VideoModel.sequelize.query<object>(query, options)
91 .then(results => results.length === 1) 91 .then(results => results.length === 1)
92 } 92 }
93 93
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 329cebd28..18f18795e 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -227,12 +227,12 @@ type AvailableForListIDsOptions = {
227 historyOfUser?: UserModel 227 historyOfUser?: UserModel
228} 228}
229 229
230@Scopes({ 230@Scopes(() => ({
231 [ ScopeNames.FOR_API ]: (options: ForAPIOptions) => { 231 [ ScopeNames.FOR_API ]: (options: ForAPIOptions) => {
232 const query: FindOptions = { 232 const query: FindOptions = {
233 where: { 233 where: {
234 id: { 234 id: {
235 [ Op.in ]: options.ids // FIXME: sequelize any seems broken 235 [ Op.in ]: options.ids // FIXME: sequelize ANY seems broken
236 } 236 }
237 }, 237 },
238 include: [ 238 include: [
@@ -486,7 +486,7 @@ type AvailableForListIDsOptions = {
486 [ ScopeNames.WITH_THUMBNAILS ]: { 486 [ ScopeNames.WITH_THUMBNAILS ]: {
487 include: [ 487 include: [
488 { 488 {
489 model: () => ThumbnailModel, 489 model: ThumbnailModel,
490 required: false 490 required: false
491 } 491 }
492 ] 492 ]
@@ -495,48 +495,48 @@ type AvailableForListIDsOptions = {
495 include: [ 495 include: [
496 { 496 {
497 attributes: [ 'accountId' ], 497 attributes: [ 'accountId' ],
498 model: () => VideoChannelModel.unscoped(), 498 model: VideoChannelModel.unscoped(),
499 required: true, 499 required: true,
500 include: [ 500 include: [
501 { 501 {
502 attributes: [ 'userId' ], 502 attributes: [ 'userId' ],
503 model: () => AccountModel.unscoped(), 503 model: AccountModel.unscoped(),
504 required: true 504 required: true
505 } 505 }
506 ] 506 ]
507 } 507 }
508 ] as any // FIXME: sequelize typings 508 ]
509 }, 509 },
510 [ ScopeNames.WITH_ACCOUNT_DETAILS ]: { 510 [ ScopeNames.WITH_ACCOUNT_DETAILS ]: {
511 include: [ 511 include: [
512 { 512 {
513 model: () => VideoChannelModel.unscoped(), 513 model: VideoChannelModel.unscoped(),
514 required: true, 514 required: true,
515 include: [ 515 include: [
516 { 516 {
517 attributes: { 517 attributes: {
518 exclude: [ 'privateKey', 'publicKey' ] 518 exclude: [ 'privateKey', 'publicKey' ]
519 }, 519 },
520 model: () => ActorModel.unscoped(), 520 model: ActorModel.unscoped(),
521 required: true, 521 required: true,
522 include: [ 522 include: [
523 { 523 {
524 attributes: [ 'host' ], 524 attributes: [ 'host' ],
525 model: () => ServerModel.unscoped(), 525 model: ServerModel.unscoped(),
526 required: false 526 required: false
527 }, 527 },
528 { 528 {
529 model: () => AvatarModel.unscoped(), 529 model: AvatarModel.unscoped(),
530 required: false 530 required: false
531 } 531 }
532 ] 532 ]
533 }, 533 },
534 { 534 {
535 model: () => AccountModel.unscoped(), 535 model: AccountModel.unscoped(),
536 required: true, 536 required: true,
537 include: [ 537 include: [
538 { 538 {
539 model: () => ActorModel.unscoped(), 539 model: ActorModel.unscoped(),
540 attributes: { 540 attributes: {
541 exclude: [ 'privateKey', 'publicKey' ] 541 exclude: [ 'privateKey', 'publicKey' ]
542 }, 542 },
@@ -544,11 +544,11 @@ type AvailableForListIDsOptions = {
544 include: [ 544 include: [
545 { 545 {
546 attributes: [ 'host' ], 546 attributes: [ 'host' ],
547 model: () => ServerModel.unscoped(), 547 model: ServerModel.unscoped(),
548 required: false 548 required: false
549 }, 549 },
550 { 550 {
551 model: () => AvatarModel.unscoped(), 551 model: AvatarModel.unscoped(),
552 required: false 552 required: false
553 } 553 }
554 ] 554 ]
@@ -557,16 +557,16 @@ type AvailableForListIDsOptions = {
557 } 557 }
558 ] 558 ]
559 } 559 }
560 ] as any // FIXME: sequelize typings 560 ]
561 }, 561 },
562 [ ScopeNames.WITH_TAGS ]: { 562 [ ScopeNames.WITH_TAGS ]: {
563 include: [ () => TagModel ] 563 include: [ TagModel ]
564 }, 564 },
565 [ ScopeNames.WITH_BLACKLISTED ]: { 565 [ ScopeNames.WITH_BLACKLISTED ]: {
566 include: [ 566 include: [
567 { 567 {
568 attributes: [ 'id', 'reason' ], 568 attributes: [ 'id', 'reason' ],
569 model: () => VideoBlacklistModel, 569 model: VideoBlacklistModel,
570 required: false 570 required: false
571 } 571 }
572 ] 572 ]
@@ -588,8 +588,7 @@ type AvailableForListIDsOptions = {
588 include: [ 588 include: [
589 { 589 {
590 model: VideoFileModel.unscoped(), 590 model: VideoFileModel.unscoped(),
591 // FIXME: typings 591 separate: true, // We may have multiple files, having multiple redundancies so let's separate this join
592 [ 'separate' as any ]: true, // We may have multiple files, having multiple redundancies so let's separate this join
593 required: false, 592 required: false,
594 include: subInclude 593 include: subInclude
595 } 594 }
@@ -613,8 +612,7 @@ type AvailableForListIDsOptions = {
613 include: [ 612 include: [
614 { 613 {
615 model: VideoStreamingPlaylistModel.unscoped(), 614 model: VideoStreamingPlaylistModel.unscoped(),
616 // FIXME: typings 615 separate: true, // We may have multiple streaming playlists, having multiple redundancies so let's separate this join
617 [ 'separate' as any ]: true, // We may have multiple streaming playlists, having multiple redundancies so let's separate this join
618 required: false, 616 required: false,
619 include: subInclude 617 include: subInclude
620 } 618 }
@@ -624,7 +622,7 @@ type AvailableForListIDsOptions = {
624 [ ScopeNames.WITH_SCHEDULED_UPDATE ]: { 622 [ ScopeNames.WITH_SCHEDULED_UPDATE ]: {
625 include: [ 623 include: [
626 { 624 {
627 model: () => ScheduleVideoUpdateModel.unscoped(), 625 model: ScheduleVideoUpdateModel.unscoped(),
628 required: false 626 required: false
629 } 627 }
630 ] 628 ]
@@ -643,7 +641,7 @@ type AvailableForListIDsOptions = {
643 ] 641 ]
644 } 642 }
645 } 643 }
646}) 644}))
647@Table({ 645@Table({
648 tableName: 'video', 646 tableName: 'video',
649 indexes 647 indexes
@@ -1075,15 +1073,14 @@ export class VideoModel extends Model<VideoModel> {
1075 } 1073 }
1076 1074
1077 return Bluebird.all([ 1075 return Bluebird.all([
1078 // FIXME: typing issue 1076 VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findAll(query),
1079 VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findAll(query as any), 1077 VideoModel.sequelize.query<{ total: string }>(rawCountQuery, { type: QueryTypes.SELECT })
1080 VideoModel.sequelize.query<{ total: number }>(rawCountQuery, { type: QueryTypes.SELECT })
1081 ]).then(([ rows, totals ]) => { 1078 ]).then(([ rows, totals ]) => {
1082 // totals: totalVideos + totalVideoShares 1079 // totals: totalVideos + totalVideoShares
1083 let totalVideos = 0 1080 let totalVideos = 0
1084 let totalVideoShares = 0 1081 let totalVideoShares = 0
1085 if (totals[ 0 ]) totalVideos = parseInt(totals[ 0 ].total + '', 10) 1082 if (totals[ 0 ]) totalVideos = parseInt(totals[ 0 ].total, 10)
1086 if (totals[ 1 ]) totalVideoShares = parseInt(totals[ 1 ].total + '', 10) 1083 if (totals[ 1 ]) totalVideoShares = parseInt(totals[ 1 ].total, 10)
1087 1084
1088 const total = totalVideos + totalVideoShares 1085 const total = totalVideos + totalVideoShares
1089 return { 1086 return {
@@ -1094,50 +1091,58 @@ export class VideoModel extends Model<VideoModel> {
1094 } 1091 }
1095 1092
1096 static listUserVideosForApi (accountId: number, start: number, count: number, sort: string, withFiles = false) { 1093 static listUserVideosForApi (accountId: number, start: number, count: number, sort: string, withFiles = false) {
1097 const query: FindOptions = { 1094 function buildBaseQuery (): FindOptions {
1098 offset: start, 1095 return {
1099 limit: count, 1096 offset: start,
1100 order: getVideoSort(sort), 1097 limit: count,
1101 include: [ 1098 order: getVideoSort(sort),
1102 { 1099 include: [
1103 model: VideoChannelModel, 1100 {
1104 required: true, 1101 model: VideoChannelModel,
1105 include: [ 1102 required: true,
1106 { 1103 include: [
1107 model: AccountModel, 1104 {
1108 where: { 1105 model: AccountModel,
1109 id: accountId 1106 where: {
1110 }, 1107 id: accountId
1111 required: true 1108 },
1112 } 1109 required: true
1113 ] 1110 }
1114 }, 1111 ]
1115 { 1112 }
1116 model: ScheduleVideoUpdateModel, 1113 ]
1117 required: false 1114 }
1118 },
1119 {
1120 model: VideoBlacklistModel,
1121 required: false
1122 }
1123 ]
1124 } 1115 }
1125 1116
1117 const countQuery = buildBaseQuery()
1118 const findQuery = buildBaseQuery()
1119
1120 findQuery.include.push({
1121 model: ScheduleVideoUpdateModel,
1122 required: false
1123 })
1124
1125 findQuery.include.push({
1126 model: VideoBlacklistModel,
1127 required: false
1128 })
1129
1126 if (withFiles === true) { 1130 if (withFiles === true) {
1127 query.include.push({ 1131 findQuery.include.push({
1128 model: VideoFileModel.unscoped(), 1132 model: VideoFileModel.unscoped(),
1129 required: true 1133 required: true
1130 }) 1134 })
1131 } 1135 }
1132 1136
1133 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS) 1137 return Promise.all([
1134 .findAndCountAll(query) 1138 VideoModel.count(countQuery),
1135 .then(({ rows, count }) => { 1139 VideoModel.findAll(findQuery)
1136 return { 1140 ]).then(([ count, rows ]) => {
1137 data: rows, 1141 return {
1138 total: count 1142 data: rows,
1139 } 1143 total: count
1140 }) 1144 }
1145 })
1141 } 1146 }
1142 1147
1143 static async listForApi (options: { 1148 static async listForApi (options: {
@@ -1404,12 +1409,12 @@ export class VideoModel extends Model<VideoModel> {
1404 const where = buildWhereIdOrUUID(id) 1409 const where = buildWhereIdOrUUID(id)
1405 1410
1406 const options = { 1411 const options = {
1407 order: [ [ 'Tags', 'name', 'ASC' ] ] as any, // FIXME: sequelize typings 1412 order: [ [ 'Tags', 'name', 'ASC' ] ] as any,
1408 where, 1413 where,
1409 transaction: t 1414 transaction: t
1410 } 1415 }
1411 1416
1412 const scopes = [ 1417 const scopes: (string | ScopeOptions)[] = [
1413 ScopeNames.WITH_TAGS, 1418 ScopeNames.WITH_TAGS,
1414 ScopeNames.WITH_BLACKLISTED, 1419 ScopeNames.WITH_BLACKLISTED,
1415 ScopeNames.WITH_ACCOUNT_DETAILS, 1420 ScopeNames.WITH_ACCOUNT_DETAILS,
@@ -1420,7 +1425,7 @@ export class VideoModel extends Model<VideoModel> {
1420 ] 1425 ]
1421 1426
1422 if (userId) { 1427 if (userId) {
1423 scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] } as any) // FIXME: typings 1428 scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] })
1424 } 1429 }
1425 1430
1426 return VideoModel 1431 return VideoModel
@@ -1437,18 +1442,18 @@ export class VideoModel extends Model<VideoModel> {
1437 transaction: t 1442 transaction: t
1438 } 1443 }
1439 1444
1440 const scopes = [ 1445 const scopes: (string | ScopeOptions)[] = [
1441 ScopeNames.WITH_TAGS, 1446 ScopeNames.WITH_TAGS,
1442 ScopeNames.WITH_BLACKLISTED, 1447 ScopeNames.WITH_BLACKLISTED,
1443 ScopeNames.WITH_ACCOUNT_DETAILS, 1448 ScopeNames.WITH_ACCOUNT_DETAILS,
1444 ScopeNames.WITH_SCHEDULED_UPDATE, 1449 ScopeNames.WITH_SCHEDULED_UPDATE,
1445 ScopeNames.WITH_THUMBNAILS, 1450 ScopeNames.WITH_THUMBNAILS,
1446 { method: [ ScopeNames.WITH_FILES, true ] } as any, // FIXME: typings 1451 { method: [ ScopeNames.WITH_FILES, true ] },
1447 { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] } as any // FIXME: typings 1452 { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] }
1448 ] 1453 ]
1449 1454
1450 if (userId) { 1455 if (userId) {
1451 scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] } as any) // FIXME: typings 1456 scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] })
1452 } 1457 }
1453 1458
1454 return VideoModel 1459 return VideoModel
@@ -1520,9 +1525,9 @@ export class VideoModel extends Model<VideoModel> {
1520 attributes: [ field ], 1525 attributes: [ field ],
1521 limit: count, 1526 limit: count,
1522 group: field, 1527 group: field,
1523 having: Sequelize.where(Sequelize.fn('COUNT', Sequelize.col(field)), { 1528 having: Sequelize.where(
1524 [ Op.gte ]: threshold 1529 Sequelize.fn('COUNT', Sequelize.col(field)), { [ Op.gte ]: threshold }
1525 }) as any, // FIXME: typings 1530 ),
1526 order: [ (this.sequelize as any).random() ] 1531 order: [ (this.sequelize as any).random() ]
1527 } 1532 }
1528 1533
@@ -1594,16 +1599,10 @@ export class VideoModel extends Model<VideoModel> {
1594 ] 1599 ]
1595 } 1600 }
1596 1601
1597 // FIXME: typing 1602 const apiScope: (string | ScopeOptions)[] = [ ScopeNames.WITH_THUMBNAILS ]
1598 const apiScope: any[] = [ ScopeNames.WITH_THUMBNAILS ]
1599 1603
1600 if (options.user) { 1604 if (options.user) {
1601 apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] }) 1605 apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] })
1602
1603 // Even if the relation is n:m, we know that a user only have 0..1 video history
1604 // So we won't have multiple rows for the same video
1605 // A subquery adds some bugs in our query so disable it
1606 secondQuery.subQuery = false
1607 } 1606 }
1608 1607
1609 apiScope.push({ 1608 apiScope.push({
@@ -1651,13 +1650,17 @@ export class VideoModel extends Model<VideoModel> {
1651 return maxBy(this.VideoFiles, file => file.resolution) 1650 return maxBy(this.VideoFiles, file => file.resolution)
1652 } 1651 }
1653 1652
1654 addThumbnail (thumbnail: ThumbnailModel) { 1653 async addAndSaveThumbnail (thumbnail: ThumbnailModel, transaction: Transaction) {
1654 thumbnail.videoId = this.id
1655
1656 const savedThumbnail = await thumbnail.save({ transaction })
1657
1655 if (Array.isArray(this.Thumbnails) === false) this.Thumbnails = [] 1658 if (Array.isArray(this.Thumbnails) === false) this.Thumbnails = []
1656 1659
1657 // Already have this thumbnail, skip 1660 // Already have this thumbnail, skip
1658 if (this.Thumbnails.find(t => t.id === thumbnail.id)) return 1661 if (this.Thumbnails.find(t => t.id === savedThumbnail.id)) return
1659 1662
1660 this.Thumbnails.push(thumbnail) 1663 this.Thumbnails.push(savedThumbnail)
1661 } 1664 }
1662 1665
1663 getVideoFilename (videoFile: VideoFileModel) { 1666 getVideoFilename (videoFile: VideoFileModel) {
@@ -1668,10 +1671,10 @@ export class VideoModel extends Model<VideoModel> {
1668 return this.uuid + '.jpg' 1671 return this.uuid + '.jpg'
1669 } 1672 }
1670 1673
1671 getThumbnail () { 1674 getMiniature () {
1672 if (Array.isArray(this.Thumbnails) === false) return undefined 1675 if (Array.isArray(this.Thumbnails) === false) return undefined
1673 1676
1674 return this.Thumbnails.find(t => t.type === ThumbnailType.THUMBNAIL) 1677 return this.Thumbnails.find(t => t.type === ThumbnailType.MINIATURE)
1675 } 1678 }
1676 1679
1677 generatePreviewName () { 1680 generatePreviewName () {
@@ -1732,8 +1735,8 @@ export class VideoModel extends Model<VideoModel> {
1732 return '/videos/embed/' + this.uuid 1735 return '/videos/embed/' + this.uuid
1733 } 1736 }
1734 1737
1735 getThumbnailStaticPath () { 1738 getMiniatureStaticPath () {
1736 const thumbnail = this.getThumbnail() 1739 const thumbnail = this.getMiniature()
1737 if (!thumbnail) return null 1740 if (!thumbnail) return null
1738 1741
1739 return join(STATIC_PATHS.THUMBNAILS, thumbnail.filename) 1742 return join(STATIC_PATHS.THUMBNAILS, thumbnail.filename)