aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models
diff options
context:
space:
mode:
Diffstat (limited to 'server/models')
-rw-r--r--server/models/account/account-blocklist.ts31
-rw-r--r--server/models/account/account.ts25
-rw-r--r--server/models/account/user-notification-setting.ts150
-rw-r--r--server/models/account/user-notification.ts472
-rw-r--r--server/models/account/user-video-history.ts33
-rw-r--r--server/models/account/user.ts180
-rw-r--r--server/models/activitypub/actor-follow.ts58
-rw-r--r--server/models/activitypub/actor.ts1
-rw-r--r--server/models/redundancy/video-redundancy.ts10
-rw-r--r--server/models/utils.ts6
-rw-r--r--server/models/video/video-abuse.ts24
-rw-r--r--server/models/video/video-blacklist.ts31
-rw-r--r--server/models/video/video-channel.ts25
-rw-r--r--server/models/video/video-comment.ts45
-rw-r--r--server/models/video/video-file.ts26
-rw-r--r--server/models/video/video-format-utils.ts6
-rw-r--r--server/models/video/video-import.ts4
-rw-r--r--server/models/video/video.ts112
18 files changed, 1110 insertions, 129 deletions
diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts
index fa2819235..efd6ed59e 100644
--- a/server/models/account/account-blocklist.ts
+++ b/server/models/account/account-blocklist.ts
@@ -2,6 +2,7 @@ import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, Updated
2import { AccountModel } from './account' 2import { AccountModel } from './account'
3import { getSort } from '../utils' 3import { getSort } from '../utils'
4import { AccountBlock } from '../../../shared/models/blocklist' 4import { AccountBlock } from '../../../shared/models/blocklist'
5import { Op } from 'sequelize'
5 6
6enum ScopeNames { 7enum ScopeNames {
7 WITH_ACCOUNTS = 'WITH_ACCOUNTS' 8 WITH_ACCOUNTS = 'WITH_ACCOUNTS'
@@ -72,6 +73,36 @@ export class AccountBlocklistModel extends Model<AccountBlocklistModel> {
72 }) 73 })
73 BlockedAccount: AccountModel 74 BlockedAccount: AccountModel
74 75
76 static isAccountMutedBy (accountId: number, targetAccountId: number) {
77 return AccountBlocklistModel.isAccountMutedByMulti([ accountId ], targetAccountId)
78 .then(result => result[accountId])
79 }
80
81 static isAccountMutedByMulti (accountIds: number[], targetAccountId: number) {
82 const query = {
83 attributes: [ 'accountId', 'id' ],
84 where: {
85 accountId: {
86 [Op.any]: accountIds
87 },
88 targetAccountId
89 },
90 raw: true
91 }
92
93 return AccountBlocklistModel.unscoped()
94 .findAll(query)
95 .then(rows => {
96 const result: { [accountId: number]: boolean } = {}
97
98 for (const accountId of accountIds) {
99 result[accountId] = !!rows.find(r => r.accountId === accountId)
100 }
101
102 return result
103 })
104 }
105
75 static loadByAccountAndTarget (accountId: number, targetAccountId: number) { 106 static loadByAccountAndTarget (accountId: number, targetAccountId: number) {
76 const query = { 107 const query = {
77 where: { 108 where: {
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index 5a237d733..84ef0b30d 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -241,6 +241,27 @@ export class AccountModel extends Model<AccountModel> {
241 }) 241 })
242 } 242 }
243 243
244 static listLocalsForSitemap (sort: string) {
245 const query = {
246 attributes: [ ],
247 offset: 0,
248 order: getSort(sort),
249 include: [
250 {
251 attributes: [ 'preferredUsername', 'serverId' ],
252 model: ActorModel.unscoped(),
253 where: {
254 serverId: null
255 }
256 }
257 ]
258 }
259
260 return AccountModel
261 .unscoped()
262 .findAll(query)
263 }
264
244 toFormattedJSON (): Account { 265 toFormattedJSON (): Account {
245 const actor = this.Actor.toFormattedJSON() 266 const actor = this.Actor.toFormattedJSON()
246 const account = { 267 const account = {
@@ -267,6 +288,10 @@ export class AccountModel extends Model<AccountModel> {
267 return this.Actor.isOwned() 288 return this.Actor.isOwned()
268 } 289 }
269 290
291 isOutdated () {
292 return this.Actor.isOutdated()
293 }
294
270 getDisplayName () { 295 getDisplayName () {
271 return this.name 296 return this.name
272 } 297 }
diff --git a/server/models/account/user-notification-setting.ts b/server/models/account/user-notification-setting.ts
new file mode 100644
index 000000000..f1c3ac223
--- /dev/null
+++ b/server/models/account/user-notification-setting.ts
@@ -0,0 +1,150 @@
1import {
2 AfterDestroy,
3 AfterUpdate,
4 AllowNull,
5 BelongsTo,
6 Column,
7 CreatedAt,
8 Default,
9 ForeignKey,
10 Is,
11 Model,
12 Table,
13 UpdatedAt
14} from 'sequelize-typescript'
15import { throwIfNotValid } from '../utils'
16import { UserModel } from './user'
17import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications'
18import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model'
19import { clearCacheByUserId } from '../../lib/oauth-model'
20
21@Table({
22 tableName: 'userNotificationSetting',
23 indexes: [
24 {
25 fields: [ 'userId' ],
26 unique: true
27 }
28 ]
29})
30export class UserNotificationSettingModel extends Model<UserNotificationSettingModel> {
31
32 @AllowNull(false)
33 @Default(null)
34 @Is(
35 'UserNotificationSettingNewVideoFromSubscription',
36 value => throwIfNotValid(value, isUserNotificationSettingValid, 'newVideoFromSubscription')
37 )
38 @Column
39 newVideoFromSubscription: UserNotificationSettingValue
40
41 @AllowNull(false)
42 @Default(null)
43 @Is(
44 'UserNotificationSettingNewCommentOnMyVideo',
45 value => throwIfNotValid(value, isUserNotificationSettingValid, 'newCommentOnMyVideo')
46 )
47 @Column
48 newCommentOnMyVideo: UserNotificationSettingValue
49
50 @AllowNull(false)
51 @Default(null)
52 @Is(
53 'UserNotificationSettingVideoAbuseAsModerator',
54 value => throwIfNotValid(value, isUserNotificationSettingValid, 'videoAbuseAsModerator')
55 )
56 @Column
57 videoAbuseAsModerator: UserNotificationSettingValue
58
59 @AllowNull(false)
60 @Default(null)
61 @Is(
62 'UserNotificationSettingBlacklistOnMyVideo',
63 value => throwIfNotValid(value, isUserNotificationSettingValid, 'blacklistOnMyVideo')
64 )
65 @Column
66 blacklistOnMyVideo: UserNotificationSettingValue
67
68 @AllowNull(false)
69 @Default(null)
70 @Is(
71 'UserNotificationSettingMyVideoPublished',
72 value => throwIfNotValid(value, isUserNotificationSettingValid, 'myVideoPublished')
73 )
74 @Column
75 myVideoPublished: UserNotificationSettingValue
76
77 @AllowNull(false)
78 @Default(null)
79 @Is(
80 'UserNotificationSettingMyVideoImportFinished',
81 value => throwIfNotValid(value, isUserNotificationSettingValid, 'myVideoImportFinished')
82 )
83 @Column
84 myVideoImportFinished: UserNotificationSettingValue
85
86 @AllowNull(false)
87 @Default(null)
88 @Is(
89 'UserNotificationSettingNewUserRegistration',
90 value => throwIfNotValid(value, isUserNotificationSettingValid, 'newUserRegistration')
91 )
92 @Column
93 newUserRegistration: UserNotificationSettingValue
94
95 @AllowNull(false)
96 @Default(null)
97 @Is(
98 'UserNotificationSettingNewFollow',
99 value => throwIfNotValid(value, isUserNotificationSettingValid, 'newFollow')
100 )
101 @Column
102 newFollow: UserNotificationSettingValue
103
104 @AllowNull(false)
105 @Default(null)
106 @Is(
107 'UserNotificationSettingCommentMention',
108 value => throwIfNotValid(value, isUserNotificationSettingValid, 'commentMention')
109 )
110 @Column
111 commentMention: UserNotificationSettingValue
112
113 @ForeignKey(() => UserModel)
114 @Column
115 userId: number
116
117 @BelongsTo(() => UserModel, {
118 foreignKey: {
119 allowNull: false
120 },
121 onDelete: 'cascade'
122 })
123 User: UserModel
124
125 @CreatedAt
126 createdAt: Date
127
128 @UpdatedAt
129 updatedAt: Date
130
131 @AfterUpdate
132 @AfterDestroy
133 static removeTokenCache (instance: UserNotificationSettingModel) {
134 return clearCacheByUserId(instance.userId)
135 }
136
137 toFormattedJSON (): UserNotificationSetting {
138 return {
139 newCommentOnMyVideo: this.newCommentOnMyVideo,
140 newVideoFromSubscription: this.newVideoFromSubscription,
141 videoAbuseAsModerator: this.videoAbuseAsModerator,
142 blacklistOnMyVideo: this.blacklistOnMyVideo,
143 myVideoPublished: this.myVideoPublished,
144 myVideoImportFinished: this.myVideoImportFinished,
145 newUserRegistration: this.newUserRegistration,
146 commentMention: this.commentMention,
147 newFollow: this.newFollow
148 }
149 }
150}
diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts
new file mode 100644
index 000000000..6cdbb827b
--- /dev/null
+++ b/server/models/account/user-notification.ts
@@ -0,0 +1,472 @@
1import {
2 AllowNull,
3 BelongsTo,
4 Column,
5 CreatedAt,
6 Default,
7 ForeignKey,
8 IFindOptions,
9 Is,
10 Model,
11 Scopes,
12 Table,
13 UpdatedAt
14} from 'sequelize-typescript'
15import { UserNotification, UserNotificationType } from '../../../shared'
16import { getSort, throwIfNotValid } from '../utils'
17import { isBooleanValid } from '../../helpers/custom-validators/misc'
18import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications'
19import { UserModel } from './user'
20import { VideoModel } from '../video/video'
21import { VideoCommentModel } from '../video/video-comment'
22import { Op } from 'sequelize'
23import { VideoChannelModel } from '../video/video-channel'
24import { AccountModel } from './account'
25import { VideoAbuseModel } from '../video/video-abuse'
26import { VideoBlacklistModel } from '../video/video-blacklist'
27import { VideoImportModel } from '../video/video-import'
28import { ActorModel } from '../activitypub/actor'
29import { ActorFollowModel } from '../activitypub/actor-follow'
30import { AvatarModel } from '../avatar/avatar'
31import { ServerModel } from '../server/server'
32
33enum ScopeNames {
34 WITH_ALL = 'WITH_ALL'
35}
36
37function buildActorWithAvatarInclude () {
38 return {
39 attributes: [ 'preferredUsername' ],
40 model: () => ActorModel.unscoped(),
41 required: true,
42 include: [
43 {
44 attributes: [ 'filename' ],
45 model: () => AvatarModel.unscoped(),
46 required: false
47 },
48 {
49 attributes: [ 'host' ],
50 model: () => ServerModel.unscoped(),
51 required: false
52 }
53 ]
54 }
55}
56
57function buildVideoInclude (required: boolean) {
58 return {
59 attributes: [ 'id', 'uuid', 'name' ],
60 model: () => VideoModel.unscoped(),
61 required
62 }
63}
64
65function buildChannelInclude (required: boolean, withActor = false) {
66 return {
67 required,
68 attributes: [ 'id', 'name' ],
69 model: () => VideoChannelModel.unscoped(),
70 include: withActor === true ? [ buildActorWithAvatarInclude() ] : []
71 }
72}
73
74function buildAccountInclude (required: boolean, withActor = false) {
75 return {
76 required,
77 attributes: [ 'id', 'name' ],
78 model: () => AccountModel.unscoped(),
79 include: withActor === true ? [ buildActorWithAvatarInclude() ] : []
80 }
81}
82
83@Scopes({
84 [ScopeNames.WITH_ALL]: {
85 include: [
86 Object.assign(buildVideoInclude(false), {
87 include: [ buildChannelInclude(true, true) ]
88 }),
89
90 {
91 attributes: [ 'id', 'originCommentId' ],
92 model: () => VideoCommentModel.unscoped(),
93 required: false,
94 include: [
95 buildAccountInclude(true, true),
96 buildVideoInclude(true)
97 ]
98 },
99
100 {
101 attributes: [ 'id' ],
102 model: () => VideoAbuseModel.unscoped(),
103 required: false,
104 include: [ buildVideoInclude(true) ]
105 },
106
107 {
108 attributes: [ 'id' ],
109 model: () => VideoBlacklistModel.unscoped(),
110 required: false,
111 include: [ buildVideoInclude(true) ]
112 },
113
114 {
115 attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ],
116 model: () => VideoImportModel.unscoped(),
117 required: false,
118 include: [ buildVideoInclude(false) ]
119 },
120
121 {
122 attributes: [ 'id' ],
123 model: () => ActorFollowModel.unscoped(),
124 required: false,
125 include: [
126 {
127 attributes: [ 'preferredUsername' ],
128 model: () => ActorModel.unscoped(),
129 required: true,
130 as: 'ActorFollower',
131 include: [
132 {
133 attributes: [ 'id', 'name' ],
134 model: () => AccountModel.unscoped(),
135 required: true
136 },
137 {
138 attributes: [ 'filename' ],
139 model: () => AvatarModel.unscoped(),
140 required: false
141 },
142 {
143 attributes: [ 'host' ],
144 model: () => ServerModel.unscoped(),
145 required: false
146 }
147 ]
148 },
149 {
150 attributes: [ 'preferredUsername' ],
151 model: () => ActorModel.unscoped(),
152 required: true,
153 as: 'ActorFollowing',
154 include: [
155 buildChannelInclude(false),
156 buildAccountInclude(false)
157 ]
158 }
159 ]
160 },
161
162 buildAccountInclude(false, true)
163 ]
164 }
165})
166@Table({
167 tableName: 'userNotification',
168 indexes: [
169 {
170 fields: [ 'userId' ]
171 },
172 {
173 fields: [ 'videoId' ],
174 where: {
175 videoId: {
176 [Op.ne]: null
177 }
178 }
179 },
180 {
181 fields: [ 'commentId' ],
182 where: {
183 commentId: {
184 [Op.ne]: null
185 }
186 }
187 },
188 {
189 fields: [ 'videoAbuseId' ],
190 where: {
191 videoAbuseId: {
192 [Op.ne]: null
193 }
194 }
195 },
196 {
197 fields: [ 'videoBlacklistId' ],
198 where: {
199 videoBlacklistId: {
200 [Op.ne]: null
201 }
202 }
203 },
204 {
205 fields: [ 'videoImportId' ],
206 where: {
207 videoImportId: {
208 [Op.ne]: null
209 }
210 }
211 },
212 {
213 fields: [ 'accountId' ],
214 where: {
215 accountId: {
216 [Op.ne]: null
217 }
218 }
219 },
220 {
221 fields: [ 'actorFollowId' ],
222 where: {
223 actorFollowId: {
224 [Op.ne]: null
225 }
226 }
227 }
228 ]
229})
230export class UserNotificationModel extends Model<UserNotificationModel> {
231
232 @AllowNull(false)
233 @Default(null)
234 @Is('UserNotificationType', value => throwIfNotValid(value, isUserNotificationTypeValid, 'type'))
235 @Column
236 type: UserNotificationType
237
238 @AllowNull(false)
239 @Default(false)
240 @Is('UserNotificationRead', value => throwIfNotValid(value, isBooleanValid, 'read'))
241 @Column
242 read: boolean
243
244 @CreatedAt
245 createdAt: Date
246
247 @UpdatedAt
248 updatedAt: Date
249
250 @ForeignKey(() => UserModel)
251 @Column
252 userId: number
253
254 @BelongsTo(() => UserModel, {
255 foreignKey: {
256 allowNull: false
257 },
258 onDelete: 'cascade'
259 })
260 User: UserModel
261
262 @ForeignKey(() => VideoModel)
263 @Column
264 videoId: number
265
266 @BelongsTo(() => VideoModel, {
267 foreignKey: {
268 allowNull: true
269 },
270 onDelete: 'cascade'
271 })
272 Video: VideoModel
273
274 @ForeignKey(() => VideoCommentModel)
275 @Column
276 commentId: number
277
278 @BelongsTo(() => VideoCommentModel, {
279 foreignKey: {
280 allowNull: true
281 },
282 onDelete: 'cascade'
283 })
284 Comment: VideoCommentModel
285
286 @ForeignKey(() => VideoAbuseModel)
287 @Column
288 videoAbuseId: number
289
290 @BelongsTo(() => VideoAbuseModel, {
291 foreignKey: {
292 allowNull: true
293 },
294 onDelete: 'cascade'
295 })
296 VideoAbuse: VideoAbuseModel
297
298 @ForeignKey(() => VideoBlacklistModel)
299 @Column
300 videoBlacklistId: number
301
302 @BelongsTo(() => VideoBlacklistModel, {
303 foreignKey: {
304 allowNull: true
305 },
306 onDelete: 'cascade'
307 })
308 VideoBlacklist: VideoBlacklistModel
309
310 @ForeignKey(() => VideoImportModel)
311 @Column
312 videoImportId: number
313
314 @BelongsTo(() => VideoImportModel, {
315 foreignKey: {
316 allowNull: true
317 },
318 onDelete: 'cascade'
319 })
320 VideoImport: VideoImportModel
321
322 @ForeignKey(() => AccountModel)
323 @Column
324 accountId: number
325
326 @BelongsTo(() => AccountModel, {
327 foreignKey: {
328 allowNull: true
329 },
330 onDelete: 'cascade'
331 })
332 Account: AccountModel
333
334 @ForeignKey(() => ActorFollowModel)
335 @Column
336 actorFollowId: number
337
338 @BelongsTo(() => ActorFollowModel, {
339 foreignKey: {
340 allowNull: true
341 },
342 onDelete: 'cascade'
343 })
344 ActorFollow: ActorFollowModel
345
346 static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
347 const query: IFindOptions<UserNotificationModel> = {
348 offset: start,
349 limit: count,
350 order: getSort(sort),
351 where: {
352 userId
353 }
354 }
355
356 if (unread !== undefined) query.where['read'] = !unread
357
358 return UserNotificationModel.scope(ScopeNames.WITH_ALL)
359 .findAndCountAll(query)
360 .then(({ rows, count }) => {
361 return {
362 data: rows,
363 total: count
364 }
365 })
366 }
367
368 static markAsRead (userId: number, notificationIds: number[]) {
369 const query = {
370 where: {
371 userId,
372 id: {
373 [Op.any]: notificationIds
374 }
375 }
376 }
377
378 return UserNotificationModel.update({ read: true }, query)
379 }
380
381 static markAllAsRead (userId: number) {
382 const query = { where: { userId } }
383
384 return UserNotificationModel.update({ read: true }, query)
385 }
386
387 toFormattedJSON (): UserNotification {
388 const video = this.Video
389 ? Object.assign(this.formatVideo(this.Video),{ channel: this.formatActor(this.Video.VideoChannel) })
390 : undefined
391
392 const videoImport = this.VideoImport ? {
393 id: this.VideoImport.id,
394 video: this.VideoImport.Video ? this.formatVideo(this.VideoImport.Video) : undefined,
395 torrentName: this.VideoImport.torrentName,
396 magnetUri: this.VideoImport.magnetUri,
397 targetUrl: this.VideoImport.targetUrl
398 } : undefined
399
400 const comment = this.Comment ? {
401 id: this.Comment.id,
402 threadId: this.Comment.getThreadId(),
403 account: this.formatActor(this.Comment.Account),
404 video: this.formatVideo(this.Comment.Video)
405 } : undefined
406
407 const videoAbuse = this.VideoAbuse ? {
408 id: this.VideoAbuse.id,
409 video: this.formatVideo(this.VideoAbuse.Video)
410 } : undefined
411
412 const videoBlacklist = this.VideoBlacklist ? {
413 id: this.VideoBlacklist.id,
414 video: this.formatVideo(this.VideoBlacklist.Video)
415 } : undefined
416
417 const account = this.Account ? this.formatActor(this.Account) : undefined
418
419 const actorFollow = this.ActorFollow ? {
420 id: this.ActorFollow.id,
421 follower: {
422 id: this.ActorFollow.ActorFollower.Account.id,
423 displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(),
424 name: this.ActorFollow.ActorFollower.preferredUsername,
425 avatar: this.ActorFollow.ActorFollower.Avatar ? { path: this.ActorFollow.ActorFollower.Avatar.getWebserverPath() } : undefined,
426 host: this.ActorFollow.ActorFollower.getHost()
427 },
428 following: {
429 type: this.ActorFollow.ActorFollowing.VideoChannel ? 'channel' as 'channel' : 'account' as 'account',
430 displayName: (this.ActorFollow.ActorFollowing.VideoChannel || this.ActorFollow.ActorFollowing.Account).getDisplayName(),
431 name: this.ActorFollow.ActorFollowing.preferredUsername
432 }
433 } : undefined
434
435 return {
436 id: this.id,
437 type: this.type,
438 read: this.read,
439 video,
440 videoImport,
441 comment,
442 videoAbuse,
443 videoBlacklist,
444 account,
445 actorFollow,
446 createdAt: this.createdAt.toISOString(),
447 updatedAt: this.updatedAt.toISOString()
448 }
449 }
450
451 private formatVideo (video: VideoModel) {
452 return {
453 id: video.id,
454 uuid: video.uuid,
455 name: video.name
456 }
457 }
458
459 private formatActor (accountOrChannel: AccountModel | VideoChannelModel) {
460 const avatar = accountOrChannel.Actor.Avatar
461 ? { path: accountOrChannel.Actor.Avatar.getWebserverPath() }
462 : undefined
463
464 return {
465 id: accountOrChannel.id,
466 displayName: accountOrChannel.getDisplayName(),
467 name: accountOrChannel.Actor.preferredUsername,
468 host: accountOrChannel.Actor.getHost(),
469 avatar
470 }
471 }
472}
diff --git a/server/models/account/user-video-history.ts b/server/models/account/user-video-history.ts
index 0476cad9d..15cb399c9 100644
--- a/server/models/account/user-video-history.ts
+++ b/server/models/account/user-video-history.ts
@@ -1,6 +1,7 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Min, Model, Table, UpdatedAt } from 'sequelize-typescript' 1import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { VideoModel } from '../video/video' 2import { VideoModel } from '../video/video'
3import { UserModel } from './user' 3import { UserModel } from './user'
4import { Transaction, Op, DestroyOptions } from 'sequelize'
4 5
5@Table({ 6@Table({
6 tableName: 'userVideoHistory', 7 tableName: 'userVideoHistory',
@@ -52,4 +53,34 @@ export class UserVideoHistoryModel extends Model<UserVideoHistoryModel> {
52 onDelete: 'CASCADE' 53 onDelete: 'CASCADE'
53 }) 54 })
54 User: UserModel 55 User: UserModel
56
57 static listForApi (user: UserModel, start: number, count: number) {
58 return VideoModel.listForApi({
59 start,
60 count,
61 sort: '-UserVideoHistories.updatedAt',
62 nsfw: null, // All
63 includeLocalVideos: true,
64 withFiles: false,
65 user,
66 historyOfUser: user
67 })
68 }
69
70 static removeHistoryBefore (user: UserModel, beforeDate: string, t: Transaction) {
71 const query: DestroyOptions = {
72 where: {
73 userId: user.id
74 },
75 transaction: t
76 }
77
78 if (beforeDate) {
79 query.where.updatedAt = {
80 [Op.lt]: beforeDate
81 }
82 }
83
84 return UserVideoHistoryModel.destroy(query)
85 }
55} 86}
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index 1843603f1..017a96657 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -32,6 +32,7 @@ import {
32 isUserUsernameValid, 32 isUserUsernameValid,
33 isUserVideoQuotaDailyValid, 33 isUserVideoQuotaDailyValid,
34 isUserVideoQuotaValid, 34 isUserVideoQuotaValid,
35 isUserVideosHistoryEnabledValid,
35 isUserWebTorrentEnabledValid 36 isUserWebTorrentEnabledValid
36} from '../../helpers/custom-validators/users' 37} from '../../helpers/custom-validators/users'
37import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' 38import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
@@ -43,6 +44,11 @@ import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
43import { values } from 'lodash' 44import { values } from 'lodash'
44import { NSFW_POLICY_TYPES } from '../../initializers' 45import { NSFW_POLICY_TYPES } from '../../initializers'
45import { clearCacheByUserId } from '../../lib/oauth-model' 46import { clearCacheByUserId } from '../../lib/oauth-model'
47import { UserNotificationSettingModel } from './user-notification-setting'
48import { VideoModel } from '../video/video'
49import { ActorModel } from '../activitypub/actor'
50import { ActorFollowModel } from '../activitypub/actor-follow'
51import { VideoImportModel } from '../video/video-import'
46 52
47enum ScopeNames { 53enum ScopeNames {
48 WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL' 54 WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL'
@@ -53,6 +59,10 @@ enum ScopeNames {
53 { 59 {
54 model: () => AccountModel, 60 model: () => AccountModel,
55 required: true 61 required: true
62 },
63 {
64 model: () => UserNotificationSettingModel,
65 required: true
56 } 66 }
57 ] 67 ]
58}) 68})
@@ -63,6 +73,10 @@ enum ScopeNames {
63 model: () => AccountModel, 73 model: () => AccountModel,
64 required: true, 74 required: true,
65 include: [ () => VideoChannelModel ] 75 include: [ () => VideoChannelModel ]
76 },
77 {
78 model: () => UserNotificationSettingModel,
79 required: true
66 } 80 }
67 ] 81 ]
68 } 82 }
@@ -116,6 +130,12 @@ export class UserModel extends Model<UserModel> {
116 130
117 @AllowNull(false) 131 @AllowNull(false)
118 @Default(true) 132 @Default(true)
133 @Is('UserVideosHistoryEnabled', value => throwIfNotValid(value, isUserVideosHistoryEnabledValid, 'Videos history enabled'))
134 @Column
135 videosHistoryEnabled: boolean
136
137 @AllowNull(false)
138 @Default(true)
119 @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean')) 139 @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean'))
120 @Column 140 @Column
121 autoPlayVideo: boolean 141 autoPlayVideo: boolean
@@ -160,6 +180,19 @@ export class UserModel extends Model<UserModel> {
160 }) 180 })
161 Account: AccountModel 181 Account: AccountModel
162 182
183 @HasOne(() => UserNotificationSettingModel, {
184 foreignKey: 'userId',
185 onDelete: 'cascade',
186 hooks: true
187 })
188 NotificationSetting: UserNotificationSettingModel
189
190 @HasMany(() => VideoImportModel, {
191 foreignKey: 'userId',
192 onDelete: 'cascade'
193 })
194 VideoImports: VideoImportModel[]
195
163 @HasMany(() => OAuthTokenModel, { 196 @HasMany(() => OAuthTokenModel, {
164 foreignKey: 'userId', 197 foreignKey: 'userId',
165 onDelete: 'cascade' 198 onDelete: 'cascade'
@@ -242,13 +275,12 @@ export class UserModel extends Model<UserModel> {
242 }) 275 })
243 } 276 }
244 277
245 static listEmailsWithRight (right: UserRight) { 278 static listWithRight (right: UserRight) {
246 const roles = Object.keys(USER_ROLE_LABELS) 279 const roles = Object.keys(USER_ROLE_LABELS)
247 .map(k => parseInt(k, 10) as UserRole) 280 .map(k => parseInt(k, 10) as UserRole)
248 .filter(role => hasUserRight(role, right)) 281 .filter(role => hasUserRight(role, right))
249 282
250 const query = { 283 const query = {
251 attribute: [ 'email' ],
252 where: { 284 where: {
253 role: { 285 role: {
254 [Sequelize.Op.in]: roles 286 [Sequelize.Op.in]: roles
@@ -256,9 +288,56 @@ export class UserModel extends Model<UserModel> {
256 } 288 }
257 } 289 }
258 290
259 return UserModel.unscoped() 291 return UserModel.findAll(query)
260 .findAll(query) 292 }
261 .then(u => u.map(u => u.email)) 293
294 static listUserSubscribersOf (actorId: number) {
295 const query = {
296 include: [
297 {
298 model: UserNotificationSettingModel.unscoped(),
299 required: true
300 },
301 {
302 attributes: [ 'userId' ],
303 model: AccountModel.unscoped(),
304 required: true,
305 include: [
306 {
307 attributes: [ ],
308 model: ActorModel.unscoped(),
309 required: true,
310 where: {
311 serverId: null
312 },
313 include: [
314 {
315 attributes: [ ],
316 as: 'ActorFollowings',
317 model: ActorFollowModel.unscoped(),
318 required: true,
319 where: {
320 targetActorId: actorId
321 }
322 }
323 ]
324 }
325 ]
326 }
327 ]
328 }
329
330 return UserModel.unscoped().findAll(query)
331 }
332
333 static listByUsernames (usernames: string[]) {
334 const query = {
335 where: {
336 username: usernames
337 }
338 }
339
340 return UserModel.findAll(query)
262 } 341 }
263 342
264 static loadById (id: number) { 343 static loadById (id: number) {
@@ -307,6 +386,95 @@ export class UserModel extends Model<UserModel> {
307 return UserModel.findOne(query) 386 return UserModel.findOne(query)
308 } 387 }
309 388
389 static loadByVideoId (videoId: number) {
390 const query = {
391 include: [
392 {
393 required: true,
394 attributes: [ 'id' ],
395 model: AccountModel.unscoped(),
396 include: [
397 {
398 required: true,
399 attributes: [ 'id' ],
400 model: VideoChannelModel.unscoped(),
401 include: [
402 {
403 required: true,
404 attributes: [ 'id' ],
405 model: VideoModel.unscoped(),
406 where: {
407 id: videoId
408 }
409 }
410 ]
411 }
412 ]
413 }
414 ]
415 }
416
417 return UserModel.findOne(query)
418 }
419
420 static loadByVideoImportId (videoImportId: number) {
421 const query = {
422 include: [
423 {
424 required: true,
425 attributes: [ 'id' ],
426 model: VideoImportModel.unscoped(),
427 where: {
428 id: videoImportId
429 }
430 }
431 ]
432 }
433
434 return UserModel.findOne(query)
435 }
436
437 static loadByChannelActorId (videoChannelActorId: number) {
438 const query = {
439 include: [
440 {
441 required: true,
442 attributes: [ 'id' ],
443 model: AccountModel.unscoped(),
444 include: [
445 {
446 required: true,
447 attributes: [ 'id' ],
448 model: VideoChannelModel.unscoped(),
449 where: {
450 actorId: videoChannelActorId
451 }
452 }
453 ]
454 }
455 ]
456 }
457
458 return UserModel.findOne(query)
459 }
460
461 static loadByAccountActorId (accountActorId: number) {
462 const query = {
463 include: [
464 {
465 required: true,
466 attributes: [ 'id' ],
467 model: AccountModel.unscoped(),
468 where: {
469 actorId: accountActorId
470 }
471 }
472 ]
473 }
474
475 return UserModel.findOne(query)
476 }
477
310 static getOriginalVideoFileTotalFromUser (user: UserModel) { 478 static getOriginalVideoFileTotalFromUser (user: UserModel) {
311 // Don't use sequelize because we need to use a sub query 479 // Don't use sequelize because we need to use a sub query
312 const query = UserModel.generateUserQuotaBaseSQL() 480 const query = UserModel.generateUserQuotaBaseSQL()
@@ -363,6 +531,7 @@ export class UserModel extends Model<UserModel> {
363 emailVerified: this.emailVerified, 531 emailVerified: this.emailVerified,
364 nsfwPolicy: this.nsfwPolicy, 532 nsfwPolicy: this.nsfwPolicy,
365 webTorrentEnabled: this.webTorrentEnabled, 533 webTorrentEnabled: this.webTorrentEnabled,
534 videosHistoryEnabled: this.videosHistoryEnabled,
366 autoPlayVideo: this.autoPlayVideo, 535 autoPlayVideo: this.autoPlayVideo,
367 role: this.role, 536 role: this.role,
368 roleLabel: USER_ROLE_LABELS[ this.role ], 537 roleLabel: USER_ROLE_LABELS[ this.role ],
@@ -372,6 +541,7 @@ export class UserModel extends Model<UserModel> {
372 blocked: this.blocked, 541 blocked: this.blocked,
373 blockedReason: this.blockedReason, 542 blockedReason: this.blockedReason,
374 account: this.Account.toFormattedJSON(), 543 account: this.Account.toFormattedJSON(),
544 notificationSettings: this.NotificationSetting ? this.NotificationSetting.toFormattedJSON() : undefined,
375 videoChannels: [], 545 videoChannels: [],
376 videoQuotaUsed: videoQuotaUsed !== undefined 546 videoQuotaUsed: videoQuotaUsed !== undefined
377 ? parseInt(videoQuotaUsed, 10) 547 ? parseInt(videoQuotaUsed, 10)
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts
index 0a6935083..796e07a42 100644
--- a/server/models/activitypub/actor-follow.ts
+++ b/server/models/activitypub/actor-follow.ts
@@ -127,22 +127,6 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
127 if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved) 127 if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved)
128 } 128 }
129 129
130 static updateActorFollowsScore (goodInboxes: string[], badInboxes: string[], t: Sequelize.Transaction | undefined) {
131 if (goodInboxes.length === 0 && badInboxes.length === 0) return
132
133 logger.info('Updating %d good actor follows and %d bad actor follows scores.', goodInboxes.length, badInboxes.length)
134
135 if (goodInboxes.length !== 0) {
136 ActorFollowModel.incrementScores(goodInboxes, ACTOR_FOLLOW_SCORE.BONUS, t)
137 .catch(err => logger.error('Cannot increment scores of good actor follows.', { err }))
138 }
139
140 if (badInboxes.length !== 0) {
141 ActorFollowModel.incrementScores(badInboxes, ACTOR_FOLLOW_SCORE.PENALTY, t)
142 .catch(err => logger.error('Cannot decrement scores of bad actor follows.', { err }))
143 }
144 }
145
146 static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Sequelize.Transaction) { 130 static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Sequelize.Transaction) {
147 const query = { 131 const query = {
148 where: { 132 where: {
@@ -323,7 +307,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
323 }) 307 })
324 } 308 }
325 309
326 static listFollowersForApi (id: number, start: number, count: number, sort: string, search?: string) { 310 static listFollowersForApi (actorId: number, start: number, count: number, sort: string, search?: string) {
327 const query = { 311 const query = {
328 distinct: true, 312 distinct: true,
329 offset: start, 313 offset: start,
@@ -351,7 +335,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
351 as: 'ActorFollowing', 335 as: 'ActorFollowing',
352 required: true, 336 required: true,
353 where: { 337 where: {
354 id 338 id: actorId
355 } 339 }
356 } 340 }
357 ] 341 ]
@@ -366,7 +350,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
366 }) 350 })
367 } 351 }
368 352
369 static listSubscriptionsForApi (id: number, start: number, count: number, sort: string) { 353 static listSubscriptionsForApi (actorId: number, start: number, count: number, sort: string) {
370 const query = { 354 const query = {
371 attributes: [], 355 attributes: [],
372 distinct: true, 356 distinct: true,
@@ -374,7 +358,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
374 limit: count, 358 limit: count,
375 order: getSort(sort), 359 order: getSort(sort),
376 where: { 360 where: {
377 actorId: id 361 actorId: actorId
378 }, 362 },
379 include: [ 363 include: [
380 { 364 {
@@ -464,6 +448,22 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
464 } 448 }
465 } 449 }
466 450
451 static updateFollowScore (inboxUrl: string, value: number, t?: Sequelize.Transaction) {
452 const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` +
453 'WHERE id IN (' +
454 'SELECT "actorFollow"."id" FROM "actorFollow" ' +
455 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' +
456 `WHERE "actor"."inboxUrl" = '${inboxUrl}' OR "actor"."sharedInboxUrl" = '${inboxUrl}'` +
457 ')'
458
459 const options = {
460 type: Sequelize.QueryTypes.BULKUPDATE,
461 transaction: t
462 }
463
464 return ActorFollowModel.sequelize.query(query, options)
465 }
466
467 private static async createListAcceptedFollowForApiQuery ( 467 private static async createListAcceptedFollowForApiQuery (
468 type: 'followers' | 'following', 468 type: 'followers' | 'following',
469 actorIds: number[], 469 actorIds: number[],
@@ -518,24 +518,6 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
518 } 518 }
519 } 519 }
520 520
521 private static incrementScores (inboxUrls: string[], value: number, t: Sequelize.Transaction | undefined) {
522 const inboxUrlsString = inboxUrls.map(url => `'${url}'`).join(',')
523
524 const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` +
525 'WHERE id IN (' +
526 'SELECT "actorFollow"."id" FROM "actorFollow" ' +
527 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' +
528 'WHERE "actor"."inboxUrl" IN (' + inboxUrlsString + ') OR "actor"."sharedInboxUrl" IN (' + inboxUrlsString + ')' +
529 ')'
530
531 const options = t ? {
532 type: Sequelize.QueryTypes.BULKUPDATE,
533 transaction: t
534 } : undefined
535
536 return ActorFollowModel.sequelize.query(query, options)
537 }
538
539 private static listBadActorFollows () { 521 private static listBadActorFollows () {
540 const query = { 522 const query = {
541 where: { 523 where: {
diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts
index 12b83916e..dda57a8ba 100644
--- a/server/models/activitypub/actor.ts
+++ b/server/models/activitypub/actor.ts
@@ -219,6 +219,7 @@ export class ActorModel extends Model<ActorModel> {
219 name: 'actorId', 219 name: 'actorId',
220 allowNull: false 220 allowNull: false
221 }, 221 },
222 as: 'ActorFollowings',
222 onDelete: 'cascade' 223 onDelete: 'cascade'
223 }) 224 })
224 ActorFollowing: ActorFollowModel[] 225 ActorFollowing: ActorFollowModel[]
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts
index 9de4356b4..8f2ef2d9a 100644
--- a/server/models/redundancy/video-redundancy.ts
+++ b/server/models/redundancy/video-redundancy.ts
@@ -15,7 +15,7 @@ import {
15import { ActorModel } from '../activitypub/actor' 15import { ActorModel } from '../activitypub/actor'
16import { getVideoSort, throwIfNotValid } from '../utils' 16import { getVideoSort, throwIfNotValid } from '../utils'
17import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc' 17import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc'
18import { CONFIG, CONSTRAINTS_FIELDS, VIDEO_EXT_MIMETYPE } from '../../initializers' 18import { CONFIG, CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers'
19import { VideoFileModel } from '../video/video-file' 19import { VideoFileModel } from '../video/video-file'
20import { getServerActor } from '../../helpers/utils' 20import { getServerActor } from '../../helpers/utils'
21import { VideoModel } from '../video/video' 21import { VideoModel } from '../video/video'
@@ -124,7 +124,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
124 const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` 124 const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}`
125 logger.info('Removing duplicated video file %s.', logIdentifier) 125 logger.info('Removing duplicated video file %s.', logIdentifier)
126 126
127 videoFile.Video.removeFile(videoFile) 127 videoFile.Video.removeFile(videoFile, true)
128 .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) 128 .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err }))
129 129
130 return undefined 130 return undefined
@@ -395,7 +395,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
395 ] 395 ]
396 } 396 }
397 397
398 return VideoRedundancyModel.find(query as any) // FIXME: typings 398 return VideoRedundancyModel.findOne(query as any) // FIXME: typings
399 .then((r: any) => ({ 399 .then((r: any) => ({
400 totalUsed: parseInt(r.totalUsed.toString(), 10), 400 totalUsed: parseInt(r.totalUsed.toString(), 10),
401 totalVideos: r.totalVideos, 401 totalVideos: r.totalVideos,
@@ -415,8 +415,8 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
415 expires: this.expiresOn.toISOString(), 415 expires: this.expiresOn.toISOString(),
416 url: { 416 url: {
417 type: 'Link', 417 type: 'Link',
418 mimeType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any, 418 mimeType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any,
419 mediaType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any, 419 mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any,
420 href: this.fileUrl, 420 href: this.fileUrl,
421 height: this.VideoFile.resolution, 421 height: this.VideoFile.resolution,
422 size: this.VideoFile.size, 422 size: this.VideoFile.size,
diff --git a/server/models/utils.ts b/server/models/utils.ts
index 60b0906e8..5b4093aec 100644
--- a/server/models/utils.ts
+++ b/server/models/utils.ts
@@ -29,7 +29,11 @@ function getVideoSort (value: string, lastSort: string[] = [ 'id', 'ASC' ]) {
29 ] 29 ]
30 } 30 }
31 31
32 return [ [ field, direction ], lastSort ] 32 const firstSort = typeof field === 'string' ?
33 field.split('.').concat([ direction ]) :
34 [ field, direction ]
35
36 return [ firstSort, lastSort ]
33} 37}
34 38
35function getSortOnModel (model: any, value: string, lastSort: string[] = [ 'id', 'ASC' ]) { 39function getSortOnModel (model: any, value: string, lastSort: string[] = [ 'id', 'ASC' ]) {
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts
index dbb88ca45..cc47644f2 100644
--- a/server/models/video/video-abuse.ts
+++ b/server/models/video/video-abuse.ts
@@ -1,17 +1,4 @@
1import { 1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
2 AfterCreate,
3 AllowNull,
4 BelongsTo,
5 Column,
6 CreatedAt,
7 DataType,
8 Default,
9 ForeignKey,
10 Is,
11 Model,
12 Table,
13 UpdatedAt
14} from 'sequelize-typescript'
15import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' 2import { VideoAbuseObject } from '../../../shared/models/activitypub/objects'
16import { VideoAbuse } from '../../../shared/models/videos' 3import { VideoAbuse } from '../../../shared/models/videos'
17import { 4import {
@@ -19,7 +6,6 @@ import {
19 isVideoAbuseReasonValid, 6 isVideoAbuseReasonValid,
20 isVideoAbuseStateValid 7 isVideoAbuseStateValid
21} from '../../helpers/custom-validators/video-abuses' 8} from '../../helpers/custom-validators/video-abuses'
22import { Emailer } from '../../lib/emailer'
23import { AccountModel } from '../account/account' 9import { AccountModel } from '../account/account'
24import { getSort, throwIfNotValid } from '../utils' 10import { getSort, throwIfNotValid } from '../utils'
25import { VideoModel } from './video' 11import { VideoModel } from './video'
@@ -40,8 +26,9 @@ import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers'
40export class VideoAbuseModel extends Model<VideoAbuseModel> { 26export class VideoAbuseModel extends Model<VideoAbuseModel> {
41 27
42 @AllowNull(false) 28 @AllowNull(false)
29 @Default(null)
43 @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason')) 30 @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason'))
44 @Column 31 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.REASON.max))
45 reason: string 32 reason: string
46 33
47 @AllowNull(false) 34 @AllowNull(false)
@@ -86,11 +73,6 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
86 }) 73 })
87 Video: VideoModel 74 Video: VideoModel
88 75
89 @AfterCreate
90 static sendEmailNotification (instance: VideoAbuseModel) {
91 return Emailer.Instance.addVideoAbuseReportJob(instance.videoId)
92 }
93
94 static loadByIdAndVideoId (id: number, videoId: number) { 76 static loadByIdAndVideoId (id: number, videoId: number) {
95 const query = { 77 const query = {
96 where: { 78 where: {
diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts
index 67f7cd487..3b567e488 100644
--- a/server/models/video/video-blacklist.ts
+++ b/server/models/video/video-blacklist.ts
@@ -1,21 +1,7 @@
1import { 1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
2 AfterCreate,
3 AfterDestroy,
4 AllowNull,
5 BelongsTo,
6 Column,
7 CreatedAt,
8 DataType,
9 ForeignKey,
10 Is,
11 Model,
12 Table,
13 UpdatedAt
14} from 'sequelize-typescript'
15import { getSortOnModel, SortType, throwIfNotValid } from '../utils' 2import { getSortOnModel, SortType, throwIfNotValid } from '../utils'
16import { VideoModel } from './video' 3import { VideoModel } from './video'
17import { isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist' 4import { isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist'
18import { Emailer } from '../../lib/emailer'
19import { VideoBlacklist } from '../../../shared/models/videos' 5import { VideoBlacklist } from '../../../shared/models/videos'
20import { CONSTRAINTS_FIELDS } from '../../initializers' 6import { CONSTRAINTS_FIELDS } from '../../initializers'
21 7
@@ -35,6 +21,10 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
35 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_BLACKLIST.REASON.max)) 21 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_BLACKLIST.REASON.max))
36 reason: string 22 reason: string
37 23
24 @AllowNull(false)
25 @Column
26 unfederated: boolean
27
38 @CreatedAt 28 @CreatedAt
39 createdAt: Date 29 createdAt: Date
40 30
@@ -53,16 +43,6 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
53 }) 43 })
54 Video: VideoModel 44 Video: VideoModel
55 45
56 @AfterCreate
57 static sendBlacklistEmailNotification (instance: VideoBlacklistModel) {
58 return Emailer.Instance.addVideoBlacklistReportJob(instance.videoId, instance.reason)
59 }
60
61 @AfterDestroy
62 static sendUnblacklistEmailNotification (instance: VideoBlacklistModel) {
63 return Emailer.Instance.addVideoUnblacklistReportJob(instance.videoId)
64 }
65
66 static listForApi (start: number, count: number, sort: SortType) { 46 static listForApi (start: number, count: number, sort: SortType) {
67 const query = { 47 const query = {
68 offset: start, 48 offset: start,
@@ -103,6 +83,7 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
103 createdAt: this.createdAt, 83 createdAt: this.createdAt,
104 updatedAt: this.updatedAt, 84 updatedAt: this.updatedAt,
105 reason: this.reason, 85 reason: this.reason,
86 unfederated: this.unfederated,
106 87
107 video: { 88 video: {
108 id: video.id, 89 id: video.id,
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index f4586917e..5598d80f6 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -233,6 +233,27 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
233 }) 233 })
234 } 234 }
235 235
236 static listLocalsForSitemap (sort: string) {
237 const query = {
238 attributes: [ ],
239 offset: 0,
240 order: getSort(sort),
241 include: [
242 {
243 attributes: [ 'preferredUsername', 'serverId' ],
244 model: ActorModel.unscoped(),
245 where: {
246 serverId: null
247 }
248 }
249 ]
250 }
251
252 return VideoChannelModel
253 .unscoped()
254 .findAll(query)
255 }
256
236 static searchForApi (options: { 257 static searchForApi (options: {
237 actorId: number 258 actorId: number
238 search: string 259 search: string
@@ -449,4 +470,8 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
449 getDisplayName () { 470 getDisplayName () {
450 return this.name 471 return this.name
451 } 472 }
473
474 isOutdated () {
475 return this.Actor.isOutdated()
476 }
452} 477}
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts
index dd6d08139..cf6278da7 100644
--- a/server/models/video/video-comment.ts
+++ b/server/models/video/video-comment.ts
@@ -18,7 +18,7 @@ import { ActivityTagObject } from '../../../shared/models/activitypub/objects/co
18import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' 18import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object'
19import { VideoComment } from '../../../shared/models/videos/video-comment.model' 19import { VideoComment } from '../../../shared/models/videos/video-comment.model'
20import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 20import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
21import { CONSTRAINTS_FIELDS } from '../../initializers' 21import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers'
22import { sendDeleteVideoComment } from '../../lib/activitypub/send' 22import { sendDeleteVideoComment } from '../../lib/activitypub/send'
23import { AccountModel } from '../account/account' 23import { AccountModel } from '../account/account'
24import { ActorModel } from '../activitypub/actor' 24import { ActorModel } from '../activitypub/actor'
@@ -29,6 +29,9 @@ import { VideoModel } from './video'
29import { VideoChannelModel } from './video-channel' 29import { VideoChannelModel } from './video-channel'
30import { getServerActor } from '../../helpers/utils' 30import { getServerActor } from '../../helpers/utils'
31import { UserModel } from '../account/user' 31import { UserModel } from '../account/user'
32import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor'
33import { regexpCapture } from '../../helpers/regexp'
34import { uniq } from 'lodash'
32 35
33enum ScopeNames { 36enum ScopeNames {
34 WITH_ACCOUNT = 'WITH_ACCOUNT', 37 WITH_ACCOUNT = 'WITH_ACCOUNT',
@@ -370,9 +373,11 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
370 id: { 373 id: {
371 [ Sequelize.Op.in ]: Sequelize.literal('(' + 374 [ Sequelize.Op.in ]: Sequelize.literal('(' +
372 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' + 375 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' +
373 'SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ' + comment.id + ' UNION ' + 376 `SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ${comment.id} ` +
374 'SELECT p.id, p."inReplyToCommentId" from "videoComment" p ' + 377 'UNION ' +
375 'INNER JOIN children c ON c."inReplyToCommentId" = p.id) ' + 378 'SELECT "parent"."id", "parent"."inReplyToCommentId" FROM "videoComment" "parent" ' +
379 'INNER JOIN "children" ON "children"."inReplyToCommentId" = "parent"."id"' +
380 ') ' +
376 'SELECT id FROM children' + 381 'SELECT id FROM children' +
377 ')'), 382 ')'),
378 [ Sequelize.Op.ne ]: comment.id 383 [ Sequelize.Op.ne ]: comment.id
@@ -448,6 +453,10 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
448 } 453 }
449 } 454 }
450 455
456 getCommentStaticPath () {
457 return this.Video.getWatchStaticPath() + ';threadId=' + this.getThreadId()
458 }
459
451 getThreadId (): number { 460 getThreadId (): number {
452 return this.originCommentId || this.id 461 return this.originCommentId || this.id
453 } 462 }
@@ -456,6 +465,34 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
456 return this.Account.isOwned() 465 return this.Account.isOwned()
457 } 466 }
458 467
468 extractMentions () {
469 if (!this.text) return []
470
471 const localMention = `@(${actorNameAlphabet}+)`
472 const remoteMention = `${localMention}@${CONFIG.WEBSERVER.HOST}`
473
474 const remoteMentionsRegex = new RegExp(' ' + remoteMention + ' ', 'g')
475 const localMentionsRegex = new RegExp(' ' + localMention + ' ', 'g')
476 const firstMentionRegex = new RegExp('^(?:(?:' + remoteMention + ')|(?:' + localMention + ')) ', 'g')
477 const endMentionRegex = new RegExp(' (?:(?:' + remoteMention + ')|(?:' + localMention + '))$', 'g')
478
479 return uniq(
480 [].concat(
481 regexpCapture(this.text, remoteMentionsRegex)
482 .map(([ , username ]) => username),
483
484 regexpCapture(this.text, localMentionsRegex)
485 .map(([ , username ]) => username),
486
487 regexpCapture(this.text, firstMentionRegex)
488 .map(([ , username1, username2 ]) => username1 || username2),
489
490 regexpCapture(this.text, endMentionRegex)
491 .map(([ , username1, username2 ]) => username1 || username2)
492 )
493 )
494 }
495
459 toFormattedJSON () { 496 toFormattedJSON () {
460 return { 497 return {
461 id: this.id, 498 id: this.id,
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts
index adebdf0c7..1f1b76c1e 100644
--- a/server/models/video/video-file.ts
+++ b/server/models/video/video-file.ts
@@ -1,4 +1,3 @@
1import { values } from 'lodash'
2import { 1import {
3 AllowNull, 2 AllowNull,
4 BelongsTo, 3 BelongsTo,
@@ -14,12 +13,12 @@ import {
14 UpdatedAt 13 UpdatedAt
15} from 'sequelize-typescript' 14} from 'sequelize-typescript'
16import { 15import {
16 isVideoFileExtnameValid,
17 isVideoFileInfoHashValid, 17 isVideoFileInfoHashValid,
18 isVideoFileResolutionValid, 18 isVideoFileResolutionValid,
19 isVideoFileSizeValid, 19 isVideoFileSizeValid,
20 isVideoFPSResolutionValid 20 isVideoFPSResolutionValid
21} from '../../helpers/custom-validators/videos' 21} from '../../helpers/custom-validators/videos'
22import { CONSTRAINTS_FIELDS } from '../../initializers'
23import { throwIfNotValid } from '../utils' 22import { throwIfNotValid } from '../utils'
24import { VideoModel } from './video' 23import { VideoModel } from './video'
25import * as Sequelize from 'sequelize' 24import * as Sequelize from 'sequelize'
@@ -58,7 +57,8 @@ export class VideoFileModel extends Model<VideoFileModel> {
58 size: number 57 size: number
59 58
60 @AllowNull(false) 59 @AllowNull(false)
61 @Column(DataType.ENUM(values(CONSTRAINTS_FIELDS.VIDEOS.EXTNAME))) 60 @Is('VideoFileExtname', value => throwIfNotValid(value, isVideoFileExtnameValid, 'extname'))
61 @Column
62 extname: string 62 extname: string
63 63
64 @AllowNull(false) 64 @AllowNull(false)
@@ -120,6 +120,26 @@ export class VideoFileModel extends Model<VideoFileModel> {
120 return VideoFileModel.findById(id, options) 120 return VideoFileModel.findById(id, options)
121 } 121 }
122 122
123 static async getStats () {
124 let totalLocalVideoFilesSize = await VideoFileModel.sum('size', {
125 include: [
126 {
127 attributes: [],
128 model: VideoModel.unscoped(),
129 where: {
130 remote: false
131 }
132 }
133 ]
134 } as any)
135 // Sequelize could return null...
136 if (!totalLocalVideoFilesSize) totalLocalVideoFilesSize = 0
137
138 return {
139 totalLocalVideoFilesSize
140 }
141 }
142
123 hasSameUniqueKeysThan (other: VideoFileModel) { 143 hasSameUniqueKeysThan (other: VideoFileModel) {
124 return this.fps === other.fps && 144 return this.fps === other.fps &&
125 this.resolution === other.resolution && 145 this.resolution === other.resolution &&
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts
index e3f8d525b..de0747f22 100644
--- a/server/models/video/video-format-utils.ts
+++ b/server/models/video/video-format-utils.ts
@@ -2,7 +2,7 @@ import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
2import { VideoModel } from './video' 2import { VideoModel } from './video'
3import { VideoFileModel } from './video-file' 3import { VideoFileModel } from './video-file'
4import { ActivityUrlObject, VideoTorrentObject } from '../../../shared/models/activitypub/objects' 4import { ActivityUrlObject, VideoTorrentObject } from '../../../shared/models/activitypub/objects'
5import { CONFIG, THUMBNAILS_SIZE, VIDEO_EXT_MIMETYPE } from '../../initializers' 5import { CONFIG, MIMETYPES, THUMBNAILS_SIZE } from '../../initializers'
6import { VideoCaptionModel } from './video-caption' 6import { VideoCaptionModel } from './video-caption'
7import { 7import {
8 getVideoCommentsActivityPubUrl, 8 getVideoCommentsActivityPubUrl,
@@ -207,8 +207,8 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject {
207 for (const file of video.VideoFiles) { 207 for (const file of video.VideoFiles) {
208 url.push({ 208 url.push({
209 type: 'Link', 209 type: 'Link',
210 mimeType: VIDEO_EXT_MIMETYPE[ file.extname ] as any, 210 mimeType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any,
211 mediaType: VIDEO_EXT_MIMETYPE[ file.extname ] as any, 211 mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any,
212 href: video.getVideoFileUrl(file, baseUrlHttp), 212 href: video.getVideoFileUrl(file, baseUrlHttp),
213 height: file.resolution, 213 height: file.resolution,
214 size: file.size, 214 size: file.size,
diff --git a/server/models/video/video-import.ts b/server/models/video/video-import.ts
index 8d442b3f8..c723e57c0 100644
--- a/server/models/video/video-import.ts
+++ b/server/models/video/video-import.ts
@@ -144,6 +144,10 @@ export class VideoImportModel extends Model<VideoImportModel> {
144 }) 144 })
145 } 145 }
146 146
147 getTargetIdentifier () {
148 return this.targetUrl || this.magnetUri || this.torrentName
149 }
150
147 toFormattedJSON (): VideoImport { 151 toFormattedJSON (): VideoImport {
148 const videoFormatOptions = { 152 const videoFormatOptions = {
149 completeDescription: true, 153 completeDescription: true,
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 0f18d9f0c..80a6c7832 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -94,6 +94,7 @@ import {
94import * as validator from 'validator' 94import * as validator from 'validator'
95import { UserVideoHistoryModel } from '../account/user-video-history' 95import { UserVideoHistoryModel } from '../account/user-video-history'
96import { UserModel } from '../account/user' 96import { UserModel } from '../account/user'
97import { VideoImportModel } from './video-import'
97 98
98// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation 99// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
99const indexes: Sequelize.DefineIndexesOptions[] = [ 100const indexes: Sequelize.DefineIndexesOptions[] = [
@@ -102,17 +103,45 @@ const indexes: Sequelize.DefineIndexesOptions[] = [
102 { fields: [ 'createdAt' ] }, 103 { fields: [ 'createdAt' ] },
103 { fields: [ 'publishedAt' ] }, 104 { fields: [ 'publishedAt' ] },
104 { fields: [ 'duration' ] }, 105 { fields: [ 'duration' ] },
105 { fields: [ 'category' ] },
106 { fields: [ 'licence' ] },
107 { fields: [ 'nsfw' ] },
108 { fields: [ 'language' ] },
109 { fields: [ 'waitTranscoding' ] },
110 { fields: [ 'state' ] },
111 { fields: [ 'remote' ] },
112 { fields: [ 'views' ] }, 106 { fields: [ 'views' ] },
113 { fields: [ 'likes' ] },
114 { fields: [ 'channelId' ] }, 107 { fields: [ 'channelId' ] },
115 { 108 {
109 fields: [ 'category' ], // We don't care videos with an unknown category
110 where: {
111 category: {
112 [Sequelize.Op.ne]: null
113 }
114 }
115 },
116 {
117 fields: [ 'licence' ], // We don't care videos with an unknown licence
118 where: {
119 licence: {
120 [Sequelize.Op.ne]: null
121 }
122 }
123 },
124 {
125 fields: [ 'language' ], // We don't care videos with an unknown language
126 where: {
127 language: {
128 [Sequelize.Op.ne]: null
129 }
130 }
131 },
132 {
133 fields: [ 'nsfw' ], // Most of the videos are not NSFW
134 where: {
135 nsfw: true
136 }
137 },
138 {
139 fields: [ 'remote' ], // Only index local videos
140 where: {
141 remote: false
142 }
143 },
144 {
116 fields: [ 'uuid' ], 145 fields: [ 'uuid' ],
117 unique: true 146 unique: true
118 }, 147 },
@@ -140,7 +169,7 @@ type ForAPIOptions = {
140 169
141type AvailableForListIDsOptions = { 170type AvailableForListIDsOptions = {
142 serverAccountId: number 171 serverAccountId: number
143 actorId: number 172 followerActorId: number
144 includeLocalVideos: boolean 173 includeLocalVideos: boolean
145 filter?: VideoFilter 174 filter?: VideoFilter
146 categoryOneOf?: number[] 175 categoryOneOf?: number[]
@@ -153,7 +182,8 @@ type AvailableForListIDsOptions = {
153 accountId?: number 182 accountId?: number
154 videoChannelId?: number 183 videoChannelId?: number
155 trendingDays?: number 184 trendingDays?: number
156 user?: UserModel 185 user?: UserModel,
186 historyOfUser?: UserModel
157} 187}
158 188
159@Scopes({ 189@Scopes({
@@ -315,7 +345,7 @@ type AvailableForListIDsOptions = {
315 query.include.push(videoChannelInclude) 345 query.include.push(videoChannelInclude)
316 } 346 }
317 347
318 if (options.actorId) { 348 if (options.followerActorId) {
319 let localVideosReq = '' 349 let localVideosReq = ''
320 if (options.includeLocalVideos === true) { 350 if (options.includeLocalVideos === true) {
321 localVideosReq = ' UNION ALL ' + 351 localVideosReq = ' UNION ALL ' +
@@ -327,7 +357,7 @@ type AvailableForListIDsOptions = {
327 } 357 }
328 358
329 // Force actorId to be a number to avoid SQL injections 359 // Force actorId to be a number to avoid SQL injections
330 const actorIdNumber = parseInt(options.actorId.toString(), 10) 360 const actorIdNumber = parseInt(options.followerActorId.toString(), 10)
331 query.where[ 'id' ][ Sequelize.Op.and ].push({ 361 query.where[ 'id' ][ Sequelize.Op.and ].push({
332 [ Sequelize.Op.in ]: Sequelize.literal( 362 [ Sequelize.Op.in ]: Sequelize.literal(
333 '(' + 363 '(' +
@@ -416,6 +446,21 @@ type AvailableForListIDsOptions = {
416 query.subQuery = false 446 query.subQuery = false
417 } 447 }
418 448
449 if (options.historyOfUser) {
450 query.include.push({
451 model: UserVideoHistoryModel,
452 required: true,
453 where: {
454 userId: options.historyOfUser.id
455 }
456 })
457
458 // Even if the relation is n:m, we know that a user only have 0..1 video history
459 // So we won't have multiple rows for the same video
460 // Without this, we would not be able to sort on "updatedAt" column of UserVideoHistoryModel
461 query.subQuery = false
462 }
463
419 return query 464 return query
420 }, 465 },
421 [ ScopeNames.WITH_ACCOUNT_DETAILS ]: { 466 [ ScopeNames.WITH_ACCOUNT_DETAILS ]: {
@@ -741,6 +786,15 @@ export class VideoModel extends Model<VideoModel> {
741 }) 786 })
742 VideoBlacklist: VideoBlacklistModel 787 VideoBlacklist: VideoBlacklistModel
743 788
789 @HasOne(() => VideoImportModel, {
790 foreignKey: {
791 name: 'videoId',
792 allowNull: true
793 },
794 onDelete: 'set null'
795 })
796 VideoImport: VideoImportModel
797
744 @HasMany(() => VideoCaptionModel, { 798 @HasMany(() => VideoCaptionModel, {
745 foreignKey: { 799 foreignKey: {
746 name: 'videoId', 800 name: 'videoId',
@@ -985,9 +1039,10 @@ export class VideoModel extends Model<VideoModel> {
985 filter?: VideoFilter, 1039 filter?: VideoFilter,
986 accountId?: number, 1040 accountId?: number,
987 videoChannelId?: number, 1041 videoChannelId?: number,
988 actorId?: number 1042 followerActorId?: number
989 trendingDays?: number, 1043 trendingDays?: number,
990 user?: UserModel 1044 user?: UserModel,
1045 historyOfUser?: UserModel
991 }, countVideos = true) { 1046 }, countVideos = true) {
992 if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { 1047 if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
993 throw new Error('Try to filter all-local but no user has not the see all videos right') 1048 throw new Error('Try to filter all-local but no user has not the see all videos right')
@@ -1008,11 +1063,11 @@ export class VideoModel extends Model<VideoModel> {
1008 1063
1009 const serverActor = await getServerActor() 1064 const serverActor = await getServerActor()
1010 1065
1011 // actorId === null has a meaning, so just check undefined 1066 // followerActorId === null has a meaning, so just check undefined
1012 const actorId = options.actorId !== undefined ? options.actorId : serverActor.id 1067 const followerActorId = options.followerActorId !== undefined ? options.followerActorId : serverActor.id
1013 1068
1014 const queryOptions = { 1069 const queryOptions = {
1015 actorId, 1070 followerActorId,
1016 serverAccountId: serverActor.Account.id, 1071 serverAccountId: serverActor.Account.id,
1017 nsfw: options.nsfw, 1072 nsfw: options.nsfw,
1018 categoryOneOf: options.categoryOneOf, 1073 categoryOneOf: options.categoryOneOf,
@@ -1026,6 +1081,7 @@ export class VideoModel extends Model<VideoModel> {
1026 videoChannelId: options.videoChannelId, 1081 videoChannelId: options.videoChannelId,
1027 includeLocalVideos: options.includeLocalVideos, 1082 includeLocalVideos: options.includeLocalVideos,
1028 user: options.user, 1083 user: options.user,
1084 historyOfUser: options.historyOfUser,
1029 trendingDays 1085 trendingDays
1030 } 1086 }
1031 1087
@@ -1118,7 +1174,7 @@ export class VideoModel extends Model<VideoModel> {
1118 1174
1119 const serverActor = await getServerActor() 1175 const serverActor = await getServerActor()
1120 const queryOptions = { 1176 const queryOptions = {
1121 actorId: serverActor.id, 1177 followerActorId: serverActor.id,
1122 serverAccountId: serverActor.Account.id, 1178 serverAccountId: serverActor.Account.id,
1123 includeLocalVideos: options.includeLocalVideos, 1179 includeLocalVideos: options.includeLocalVideos,
1124 nsfw: options.nsfw, 1180 nsfw: options.nsfw,
@@ -1273,11 +1329,11 @@ export class VideoModel extends Model<VideoModel> {
1273 // threshold corresponds to how many video the field should have to be returned 1329 // threshold corresponds to how many video the field should have to be returned
1274 static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { 1330 static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) {
1275 const serverActor = await getServerActor() 1331 const serverActor = await getServerActor()
1276 const actorId = serverActor.id 1332 const followerActorId = serverActor.id
1277 1333
1278 const scopeOptions: AvailableForListIDsOptions = { 1334 const scopeOptions: AvailableForListIDsOptions = {
1279 serverAccountId: serverActor.Account.id, 1335 serverAccountId: serverActor.Account.id,
1280 actorId, 1336 followerActorId,
1281 includeLocalVideos: true 1337 includeLocalVideos: true
1282 } 1338 }
1283 1339
@@ -1341,7 +1397,7 @@ export class VideoModel extends Model<VideoModel> {
1341 } 1397 }
1342 1398
1343 const [ count, rowsId ] = await Promise.all([ 1399 const [ count, rowsId ] = await Promise.all([
1344 countVideos ? VideoModel.scope(countScope).count(countQuery) : Promise.resolve(undefined), 1400 countVideos ? VideoModel.scope(countScope).count(countQuery) : Promise.resolve<number>(undefined),
1345 VideoModel.scope(idsScope).findAll(query) 1401 VideoModel.scope(idsScope).findAll(query)
1346 ]) 1402 ])
1347 const ids = rowsId.map(r => r.id) 1403 const ids = rowsId.map(r => r.id)
@@ -1481,6 +1537,10 @@ export class VideoModel extends Model<VideoModel> {
1481 videoFile.infoHash = parsedTorrent.infoHash 1537 videoFile.infoHash = parsedTorrent.infoHash
1482 } 1538 }
1483 1539
1540 getWatchStaticPath () {
1541 return '/videos/watch/' + this.uuid
1542 }
1543
1484 getEmbedStaticPath () { 1544 getEmbedStaticPath () {
1485 return '/videos/embed/' + this.uuid 1545 return '/videos/embed/' + this.uuid
1486 } 1546 }
@@ -1538,8 +1598,10 @@ export class VideoModel extends Model<VideoModel> {
1538 .catch(err => logger.warn('Cannot delete preview %s.', previewPath, { err })) 1598 .catch(err => logger.warn('Cannot delete preview %s.', previewPath, { err }))
1539 } 1599 }
1540 1600
1541 removeFile (videoFile: VideoFileModel) { 1601 removeFile (videoFile: VideoFileModel, isRedundancy = false) {
1542 const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) 1602 const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR
1603
1604 const filePath = join(baseDir, this.getVideoFilename(videoFile))
1543 return remove(filePath) 1605 return remove(filePath)
1544 .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) 1606 .catch(err => logger.warn('Cannot delete file %s.', filePath, { err }))
1545 } 1607 }
@@ -1617,6 +1679,10 @@ export class VideoModel extends Model<VideoModel> {
1617 return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) 1679 return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
1618 } 1680 }
1619 1681
1682 getVideoRedundancyUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
1683 return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getVideoFilename(videoFile)
1684 }
1685
1620 getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) { 1686 getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
1621 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile) 1687 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile)
1622 } 1688 }