diff options
Diffstat (limited to 'server/models')
22 files changed, 784 insertions, 418 deletions
diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts index 6ebe32556..e2f66d733 100644 --- a/server/models/account/account-blocklist.ts +++ b/server/models/account/account-blocklist.ts | |||
@@ -80,7 +80,7 @@ export class AccountBlocklistModel extends Model<AccountBlocklistModel> { | |||
80 | attributes: [ 'accountId', 'id' ], | 80 | attributes: [ 'accountId', 'id' ], |
81 | where: { | 81 | where: { |
82 | accountId: { | 82 | accountId: { |
83 | [Op.in]: accountIds // FIXME: sequelize ANY seems broken | 83 | [Op.in]: accountIds |
84 | }, | 84 | }, |
85 | targetAccountId | 85 | targetAccountId |
86 | }, | 86 | }, |
diff --git a/server/models/account/account-video-rate.ts b/server/models/account/account-video-rate.ts index c593595b2..8aeb486d1 100644 --- a/server/models/account/account-video-rate.ts +++ b/server/models/account/account-video-rate.ts | |||
@@ -99,7 +99,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> { | |||
99 | static loadByAccountAndVideoOrUrl (accountId: number, videoId: number, url: string, t?: Transaction): Bluebird<MAccountVideoRate> { | 99 | static loadByAccountAndVideoOrUrl (accountId: number, videoId: number, url: string, t?: Transaction): Bluebird<MAccountVideoRate> { |
100 | const options: FindOptions = { | 100 | const options: FindOptions = { |
101 | where: { | 101 | where: { |
102 | [ Op.or]: [ | 102 | [Op.or]: [ |
103 | { | 103 | { |
104 | accountId, | 104 | accountId, |
105 | videoId | 105 | videoId |
@@ -116,10 +116,10 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> { | |||
116 | } | 116 | } |
117 | 117 | ||
118 | static listByAccountForApi (options: { | 118 | static listByAccountForApi (options: { |
119 | start: number, | 119 | start: number |
120 | count: number, | 120 | count: number |
121 | sort: string, | 121 | sort: string |
122 | type?: string, | 122 | type?: string |
123 | accountId: number | 123 | accountId: number |
124 | }) { | 124 | }) { |
125 | const query: FindOptions = { | 125 | const query: FindOptions = { |
@@ -135,7 +135,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> { | |||
135 | required: true, | 135 | required: true, |
136 | include: [ | 136 | include: [ |
137 | { | 137 | { |
138 | model: VideoChannelModel.scope({ method: [VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }), | 138 | model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }), |
139 | required: true | 139 | required: true |
140 | } | 140 | } |
141 | ] | 141 | ] |
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index 8a0ffeb63..a0081f259 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -32,8 +32,9 @@ import { FindOptions, IncludeOptions, Op, Transaction, WhereOptions } from 'sequ | |||
32 | import { AccountBlocklistModel } from './account-blocklist' | 32 | import { AccountBlocklistModel } from './account-blocklist' |
33 | import { ServerBlocklistModel } from '../server/server-blocklist' | 33 | import { ServerBlocklistModel } from '../server/server-blocklist' |
34 | import { ActorFollowModel } from '../activitypub/actor-follow' | 34 | import { ActorFollowModel } from '../activitypub/actor-follow' |
35 | import { MAccountActor, MAccountDefault, MAccountSummaryFormattable, MAccountFormattable, MAccountAP } from '../../typings/models' | 35 | import { MAccountActor, MAccountAP, MAccountDefault, MAccountFormattable, MAccountSummaryFormattable } from '../../typings/models' |
36 | import * as Bluebird from 'bluebird' | 36 | import * as Bluebird from 'bluebird' |
37 | import { ModelCache } from '@server/models/model-cache' | ||
37 | 38 | ||
38 | export enum ScopeNames { | 39 | export enum ScopeNames { |
39 | SUMMARY = 'SUMMARY' | 40 | SUMMARY = 'SUMMARY' |
@@ -53,7 +54,7 @@ export type SummaryOptions = { | |||
53 | ] | 54 | ] |
54 | })) | 55 | })) |
55 | @Scopes(() => ({ | 56 | @Scopes(() => ({ |
56 | [ ScopeNames.SUMMARY ]: (options: SummaryOptions = {}) => { | 57 | [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => { |
57 | const whereActor = options.whereActor || undefined | 58 | const whereActor = options.whereActor || undefined |
58 | 59 | ||
59 | const serverInclude: IncludeOptions = { | 60 | const serverInclude: IncludeOptions = { |
@@ -218,8 +219,6 @@ export class AccountModel extends Model<AccountModel> { | |||
218 | }) | 219 | }) |
219 | BlockedAccounts: AccountBlocklistModel[] | 220 | BlockedAccounts: AccountBlocklistModel[] |
220 | 221 | ||
221 | private static cache: { [ id: string ]: any } = {} | ||
222 | |||
223 | @BeforeDestroy | 222 | @BeforeDestroy |
224 | static async sendDeleteIfOwned (instance: AccountModel, options) { | 223 | static async sendDeleteIfOwned (instance: AccountModel, options) { |
225 | if (!instance.Actor) { | 224 | if (!instance.Actor) { |
@@ -247,45 +246,43 @@ export class AccountModel extends Model<AccountModel> { | |||
247 | } | 246 | } |
248 | 247 | ||
249 | static loadLocalByName (name: string): Bluebird<MAccountDefault> { | 248 | static loadLocalByName (name: string): Bluebird<MAccountDefault> { |
250 | // The server actor never change, so we can easily cache it | 249 | const fun = () => { |
251 | if (name === SERVER_ACTOR_NAME && AccountModel.cache[name]) { | 250 | const query = { |
252 | return Bluebird.resolve(AccountModel.cache[name]) | 251 | where: { |
253 | } | 252 | [Op.or]: [ |
254 | 253 | { | |
255 | const query = { | 254 | userId: { |
256 | where: { | 255 | [Op.ne]: null |
257 | [ Op.or ]: [ | 256 | } |
258 | { | 257 | }, |
259 | userId: { | 258 | { |
260 | [ Op.ne ]: null | 259 | applicationId: { |
260 | [Op.ne]: null | ||
261 | } | ||
261 | } | 262 | } |
262 | }, | 263 | ] |
264 | }, | ||
265 | include: [ | ||
263 | { | 266 | { |
264 | applicationId: { | 267 | model: ActorModel, |
265 | [ Op.ne ]: null | 268 | required: true, |
269 | where: { | ||
270 | preferredUsername: name | ||
266 | } | 271 | } |
267 | } | 272 | } |
268 | ] | 273 | ] |
269 | }, | 274 | } |
270 | include: [ | ||
271 | { | ||
272 | model: ActorModel, | ||
273 | required: true, | ||
274 | where: { | ||
275 | preferredUsername: name | ||
276 | } | ||
277 | } | ||
278 | ] | ||
279 | } | ||
280 | 275 | ||
281 | return AccountModel.findOne(query) | 276 | return AccountModel.findOne(query) |
282 | .then(account => { | 277 | } |
283 | if (name === SERVER_ACTOR_NAME) { | ||
284 | AccountModel.cache[name] = account | ||
285 | } | ||
286 | 278 | ||
287 | return account | 279 | return ModelCache.Instance.doCache({ |
288 | }) | 280 | cacheType: 'local-account-name', |
281 | key: name, | ||
282 | fun, | ||
283 | // The server actor never change, so we can easily cache it | ||
284 | whitelist: () => name === SERVER_ACTOR_NAME | ||
285 | }) | ||
289 | } | 286 | } |
290 | 287 | ||
291 | static loadByNameAndHost (name: string, host: string): Bluebird<MAccountDefault> { | 288 | static loadByNameAndHost (name: string, host: string): Bluebird<MAccountDefault> { |
diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts index a05f30175..5a725187a 100644 --- a/server/models/account/user-notification.ts +++ b/server/models/account/user-notification.ts | |||
@@ -363,7 +363,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> { | |||
363 | where: { | 363 | where: { |
364 | userId, | 364 | userId, |
365 | id: { | 365 | id: { |
366 | [Op.in]: notificationIds // FIXME: sequelize ANY seems broken | 366 | [Op.in]: notificationIds |
367 | } | 367 | } |
368 | } | 368 | } |
369 | } | 369 | } |
@@ -379,7 +379,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> { | |||
379 | 379 | ||
380 | toFormattedJSON (this: UserNotificationModelForApi): UserNotification { | 380 | toFormattedJSON (this: UserNotificationModelForApi): UserNotification { |
381 | const video = this.Video | 381 | const video = this.Video |
382 | ? Object.assign(this.formatVideo(this.Video),{ channel: this.formatActor(this.Video.VideoChannel) }) | 382 | ? Object.assign(this.formatVideo(this.Video), { channel: this.formatActor(this.Video.VideoChannel) }) |
383 | : undefined | 383 | : undefined |
384 | 384 | ||
385 | const videoImport = this.VideoImport ? { | 385 | const videoImport = this.VideoImport ? { |
diff --git a/server/models/account/user.ts b/server/models/account/user.ts index 4c2c5e278..777f09666 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { FindOptions, literal, Op, QueryTypes, where, fn, col } from 'sequelize' | 1 | import { FindOptions, literal, Op, QueryTypes, where, fn, col, WhereOptions } from 'sequelize' |
2 | import { | 2 | import { |
3 | AfterDestroy, | 3 | AfterDestroy, |
4 | AfterUpdate, | 4 | AfterUpdate, |
@@ -49,7 +49,7 @@ import { VideoPlaylistModel } from '../video/video-playlist' | |||
49 | import { AccountModel } from './account' | 49 | import { AccountModel } from './account' |
50 | import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' | 50 | import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' |
51 | import { values } from 'lodash' | 51 | import { values } from 'lodash' |
52 | import { DEFAULT_THEME_NAME, DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants' | 52 | import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants' |
53 | import { clearCacheByUserId } from '../../lib/oauth-model' | 53 | import { clearCacheByUserId } from '../../lib/oauth-model' |
54 | import { UserNotificationSettingModel } from './user-notification-setting' | 54 | import { UserNotificationSettingModel } from './user-notification-setting' |
55 | import { VideoModel } from '../video/video' | 55 | import { VideoModel } from '../video/video' |
@@ -101,7 +101,7 @@ enum ScopeNames { | |||
101 | required: true, | 101 | required: true, |
102 | where: { | 102 | where: { |
103 | type: { | 103 | type: { |
104 | [ Op.ne ]: VideoPlaylistType.REGULAR | 104 | [Op.ne]: VideoPlaylistType.REGULAR |
105 | } | 105 | } |
106 | } | 106 | } |
107 | } | 107 | } |
@@ -186,7 +186,10 @@ export class UserModel extends Model<UserModel> { | |||
186 | 186 | ||
187 | @AllowNull(false) | 187 | @AllowNull(false) |
188 | @Default(true) | 188 | @Default(true) |
189 | @Is('UserAutoPlayNextVideoPlaylist', value => throwIfNotValid(value, isUserAutoPlayNextVideoPlaylistValid, 'auto play next video for playlists boolean')) | 189 | @Is( |
190 | 'UserAutoPlayNextVideoPlaylist', | ||
191 | value => throwIfNotValid(value, isUserAutoPlayNextVideoPlaylistValid, 'auto play next video for playlists boolean') | ||
192 | ) | ||
190 | @Column | 193 | @Column |
191 | autoPlayNextVideoPlaylist: boolean | 194 | autoPlayNextVideoPlaylist: boolean |
192 | 195 | ||
@@ -230,7 +233,7 @@ export class UserModel extends Model<UserModel> { | |||
230 | videoQuotaDaily: number | 233 | videoQuotaDaily: number |
231 | 234 | ||
232 | @AllowNull(false) | 235 | @AllowNull(false) |
233 | @Default(DEFAULT_THEME_NAME) | 236 | @Default(DEFAULT_USER_THEME_NAME) |
234 | @Is('UserTheme', value => throwIfNotValid(value, isThemeNameValid, 'theme')) | 237 | @Is('UserTheme', value => throwIfNotValid(value, isThemeNameValid, 'theme')) |
235 | @Column | 238 | @Column |
236 | theme: string | 239 | theme: string |
@@ -308,7 +311,8 @@ export class UserModel extends Model<UserModel> { | |||
308 | } | 311 | } |
309 | 312 | ||
310 | static listForApi (start: number, count: number, sort: string, search?: string) { | 313 | static listForApi (start: number, count: number, sort: string, search?: string) { |
311 | let where = undefined | 314 | let where: WhereOptions |
315 | |||
312 | if (search) { | 316 | if (search) { |
313 | where = { | 317 | where = { |
314 | [Op.or]: [ | 318 | [Op.or]: [ |
@@ -319,7 +323,7 @@ export class UserModel extends Model<UserModel> { | |||
319 | }, | 323 | }, |
320 | { | 324 | { |
321 | username: { | 325 | username: { |
322 | [ Op.iLike ]: '%' + search + '%' | 326 | [Op.iLike]: '%' + search + '%' |
323 | } | 327 | } |
324 | } | 328 | } |
325 | ] | 329 | ] |
@@ -332,14 +336,14 @@ export class UserModel extends Model<UserModel> { | |||
332 | [ | 336 | [ |
333 | literal( | 337 | literal( |
334 | '(' + | 338 | '(' + |
335 | 'SELECT COALESCE(SUM("size"), 0) ' + | 339 | 'SELECT COALESCE(SUM("size"), 0) ' + |
336 | 'FROM (' + | 340 | 'FROM (' + |
337 | 'SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' + | 341 | 'SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' + |
338 | 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' + | 342 | 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' + |
339 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + | 343 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + |
340 | 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' + | 344 | 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' + |
341 | 'WHERE "account"."userId" = "UserModel"."id" GROUP BY "video"."id"' + | 345 | 'WHERE "account"."userId" = "UserModel"."id" GROUP BY "video"."id"' + |
342 | ') t' + | 346 | ') t' + |
343 | ')' | 347 | ')' |
344 | ), | 348 | ), |
345 | 'videoQuotaUsed' | 349 | 'videoQuotaUsed' |
@@ -353,18 +357,18 @@ export class UserModel extends Model<UserModel> { | |||
353 | } | 357 | } |
354 | 358 | ||
355 | return UserModel.findAndCountAll(query) | 359 | return UserModel.findAndCountAll(query) |
356 | .then(({ rows, count }) => { | 360 | .then(({ rows, count }) => { |
357 | return { | 361 | return { |
358 | data: rows, | 362 | data: rows, |
359 | total: count | 363 | total: count |
360 | } | 364 | } |
361 | }) | 365 | }) |
362 | } | 366 | } |
363 | 367 | ||
364 | static listWithRight (right: UserRight): Bluebird<MUserDefault[]> { | 368 | static listWithRight (right: UserRight): Bluebird<MUserDefault[]> { |
365 | const roles = Object.keys(USER_ROLE_LABELS) | 369 | const roles = Object.keys(USER_ROLE_LABELS) |
366 | .map(k => parseInt(k, 10) as UserRole) | 370 | .map(k => parseInt(k, 10) as UserRole) |
367 | .filter(role => hasUserRight(role, right)) | 371 | .filter(role => hasUserRight(role, right)) |
368 | 372 | ||
369 | const query = { | 373 | const query = { |
370 | where: { | 374 | where: { |
@@ -390,7 +394,7 @@ export class UserModel extends Model<UserModel> { | |||
390 | required: true, | 394 | required: true, |
391 | include: [ | 395 | include: [ |
392 | { | 396 | { |
393 | attributes: [ ], | 397 | attributes: [], |
394 | model: ActorModel.unscoped(), | 398 | model: ActorModel.unscoped(), |
395 | required: true, | 399 | required: true, |
396 | where: { | 400 | where: { |
@@ -398,7 +402,7 @@ export class UserModel extends Model<UserModel> { | |||
398 | }, | 402 | }, |
399 | include: [ | 403 | include: [ |
400 | { | 404 | { |
401 | attributes: [ ], | 405 | attributes: [], |
402 | as: 'ActorFollowings', | 406 | as: 'ActorFollowings', |
403 | model: ActorFollowModel.unscoped(), | 407 | model: ActorFollowModel.unscoped(), |
404 | required: true, | 408 | required: true, |
@@ -433,7 +437,7 @@ export class UserModel extends Model<UserModel> { | |||
433 | static loadByUsername (username: string): Bluebird<MUserDefault> { | 437 | static loadByUsername (username: string): Bluebird<MUserDefault> { |
434 | const query = { | 438 | const query = { |
435 | where: { | 439 | where: { |
436 | username: { [ Op.iLike ]: username } | 440 | username: { [Op.iLike]: username } |
437 | } | 441 | } |
438 | } | 442 | } |
439 | 443 | ||
@@ -443,7 +447,7 @@ export class UserModel extends Model<UserModel> { | |||
443 | static loadForMeAPI (username: string): Bluebird<MUserNotifSettingChannelDefault> { | 447 | static loadForMeAPI (username: string): Bluebird<MUserNotifSettingChannelDefault> { |
444 | const query = { | 448 | const query = { |
445 | where: { | 449 | where: { |
446 | username: { [ Op.iLike ]: username } | 450 | username: { [Op.iLike]: username } |
447 | } | 451 | } |
448 | } | 452 | } |
449 | 453 | ||
@@ -465,7 +469,7 @@ export class UserModel extends Model<UserModel> { | |||
465 | 469 | ||
466 | const query = { | 470 | const query = { |
467 | where: { | 471 | where: { |
468 | [ Op.or ]: [ | 472 | [Op.or]: [ |
469 | where(fn('lower', col('username')), fn('lower', username)), | 473 | where(fn('lower', col('username')), fn('lower', username)), |
470 | 474 | ||
471 | { email } | 475 | { email } |
@@ -592,7 +596,7 @@ export class UserModel extends Model<UserModel> { | |||
592 | const query = { | 596 | const query = { |
593 | where: { | 597 | where: { |
594 | username: { | 598 | username: { |
595 | [ Op.like ]: `%${search}%` | 599 | [Op.like]: `%${search}%` |
596 | } | 600 | } |
597 | }, | 601 | }, |
598 | limit: 10 | 602 | limit: 10 |
@@ -652,7 +656,7 @@ export class UserModel extends Model<UserModel> { | |||
652 | videoLanguages: this.videoLanguages, | 656 | videoLanguages: this.videoLanguages, |
653 | 657 | ||
654 | role: this.role, | 658 | role: this.role, |
655 | roleLabel: USER_ROLE_LABELS[ this.role ], | 659 | roleLabel: USER_ROLE_LABELS[this.role], |
656 | 660 | ||
657 | videoQuota: this.videoQuota, | 661 | videoQuota: this.videoQuota, |
658 | videoQuotaDaily: this.videoQuotaDaily, | 662 | videoQuotaDaily: this.videoQuotaDaily, |
@@ -686,13 +690,13 @@ export class UserModel extends Model<UserModel> { | |||
686 | 690 | ||
687 | if (Array.isArray(this.Account.VideoChannels) === true) { | 691 | if (Array.isArray(this.Account.VideoChannels) === true) { |
688 | json.videoChannels = this.Account.VideoChannels | 692 | json.videoChannels = this.Account.VideoChannels |
689 | .map(c => c.toFormattedJSON()) | 693 | .map(c => c.toFormattedJSON()) |
690 | .sort((v1, v2) => { | 694 | .sort((v1, v2) => { |
691 | if (v1.createdAt < v2.createdAt) return -1 | 695 | if (v1.createdAt < v2.createdAt) return -1 |
692 | if (v1.createdAt === v2.createdAt) return 0 | 696 | if (v1.createdAt === v2.createdAt) return 0 |
693 | 697 | ||
694 | return 1 | 698 | return 1 |
695 | }) | 699 | }) |
696 | } | 700 | } |
697 | 701 | ||
698 | return json | 702 | return json |
@@ -702,7 +706,7 @@ export class UserModel extends Model<UserModel> { | |||
702 | const formatted = this.toFormattedJSON() | 706 | const formatted = this.toFormattedJSON() |
703 | 707 | ||
704 | const specialPlaylists = this.Account.VideoPlaylists | 708 | const specialPlaylists = this.Account.VideoPlaylists |
705 | .map(p => ({ id: p.id, name: p.name, type: p.type })) | 709 | .map(p => ({ id: p.id, name: p.name, type: p.type })) |
706 | 710 | ||
707 | return Object.assign(formatted, { specialPlaylists }) | 711 | return Object.assign(formatted, { specialPlaylists }) |
708 | } | 712 | } |
@@ -729,12 +733,12 @@ export class UserModel extends Model<UserModel> { | |||
729 | 733 | ||
730 | return 'SELECT SUM("size") AS "total" ' + | 734 | return 'SELECT SUM("size") AS "total" ' + |
731 | 'FROM (' + | 735 | 'FROM (' + |
732 | 'SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' + | 736 | 'SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' + |
733 | 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' + | 737 | 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' + |
734 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + | 738 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + |
735 | 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' + | 739 | 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' + |
736 | 'WHERE "account"."userId" = $userId ' + andWhere + | 740 | 'WHERE "account"."userId" = $userId ' + andWhere + |
737 | 'GROUP BY "video"."id"' + | 741 | 'GROUP BY "video"."id"' + |
738 | ') t' | 742 | ') t' |
739 | } | 743 | } |
740 | 744 | ||
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts index f21d2b8a2..27643704e 100644 --- a/server/models/activitypub/actor-follow.ts +++ b/server/models/activitypub/actor-follow.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { values, difference } from 'lodash' | 2 | import { difference, values } from 'lodash' |
3 | import { | 3 | import { |
4 | AfterCreate, | 4 | AfterCreate, |
5 | AfterDestroy, | 5 | AfterDestroy, |
@@ -23,7 +23,7 @@ import { logger } from '../../helpers/logger' | |||
23 | import { getServerActor } from '../../helpers/utils' | 23 | import { getServerActor } from '../../helpers/utils' |
24 | import { ACTOR_FOLLOW_SCORE, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants' | 24 | import { ACTOR_FOLLOW_SCORE, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants' |
25 | import { ServerModel } from '../server/server' | 25 | import { ServerModel } from '../server/server' |
26 | import { createSafeIn, getSort, getFollowsSort } from '../utils' | 26 | import { createSafeIn, getFollowsSort, getSort } from '../utils' |
27 | import { ActorModel, unusedActorAttributesForAPI } from './actor' | 27 | import { ActorModel, unusedActorAttributesForAPI } from './actor' |
28 | import { VideoChannelModel } from '../video/video-channel' | 28 | import { VideoChannelModel } from '../video/video-channel' |
29 | import { AccountModel } from '../account/account' | 29 | import { AccountModel } from '../account/account' |
@@ -36,7 +36,6 @@ import { | |||
36 | MActorFollowSubscriptions | 36 | MActorFollowSubscriptions |
37 | } from '@server/typings/models' | 37 | } from '@server/typings/models' |
38 | import { ActivityPubActorType } from '@shared/models' | 38 | import { ActivityPubActorType } from '@shared/models' |
39 | import { afterCommitIfTransaction } from '@server/helpers/database-utils' | ||
40 | 39 | ||
41 | @Table({ | 40 | @Table({ |
42 | tableName: 'actorFollow', | 41 | tableName: 'actorFollow', |
@@ -226,7 +225,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
226 | 225 | ||
227 | return ActorFollowModel.findOne(query) | 226 | return ActorFollowModel.findOne(query) |
228 | .then(result => { | 227 | .then(result => { |
229 | if (result && result.ActorFollowing.VideoChannel) { | 228 | if (result?.ActorFollowing.VideoChannel) { |
230 | result.ActorFollowing.VideoChannel.Actor = result.ActorFollowing | 229 | result.ActorFollowing.VideoChannel.Actor = result.ActorFollowing |
231 | } | 230 | } |
232 | 231 | ||
@@ -239,24 +238,24 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
239 | .map(t => { | 238 | .map(t => { |
240 | if (t.host) { | 239 | if (t.host) { |
241 | return { | 240 | return { |
242 | [ Op.and ]: [ | 241 | [Op.and]: [ |
243 | { | 242 | { |
244 | '$preferredUsername$': t.name | 243 | $preferredUsername$: t.name |
245 | }, | 244 | }, |
246 | { | 245 | { |
247 | '$host$': t.host | 246 | $host$: t.host |
248 | } | 247 | } |
249 | ] | 248 | ] |
250 | } | 249 | } |
251 | } | 250 | } |
252 | 251 | ||
253 | return { | 252 | return { |
254 | [ Op.and ]: [ | 253 | [Op.and]: [ |
255 | { | 254 | { |
256 | '$preferredUsername$': t.name | 255 | $preferredUsername$: t.name |
257 | }, | 256 | }, |
258 | { | 257 | { |
259 | '$serverId$': null | 258 | $serverId$: null |
260 | } | 259 | } |
261 | ] | 260 | ] |
262 | } | 261 | } |
@@ -265,9 +264,9 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
265 | const query = { | 264 | const query = { |
266 | attributes: [], | 265 | attributes: [], |
267 | where: { | 266 | where: { |
268 | [ Op.and ]: [ | 267 | [Op.and]: [ |
269 | { | 268 | { |
270 | [ Op.or ]: whereTab | 269 | [Op.or]: whereTab |
271 | }, | 270 | }, |
272 | { | 271 | { |
273 | actorId | 272 | actorId |
@@ -295,12 +294,12 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
295 | } | 294 | } |
296 | 295 | ||
297 | static listFollowingForApi (options: { | 296 | static listFollowingForApi (options: { |
298 | id: number, | 297 | id: number |
299 | start: number, | 298 | start: number |
300 | count: number, | 299 | count: number |
301 | sort: string, | 300 | sort: string |
302 | state?: FollowState, | 301 | state?: FollowState |
303 | actorType?: ActivityPubActorType, | 302 | actorType?: ActivityPubActorType |
304 | search?: string | 303 | search?: string |
305 | }) { | 304 | }) { |
306 | const { id, start, count, sort, search, state, actorType } = options | 305 | const { id, start, count, sort, search, state, actorType } = options |
@@ -312,7 +311,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
312 | if (search) { | 311 | if (search) { |
313 | Object.assign(followingServerWhere, { | 312 | Object.assign(followingServerWhere, { |
314 | host: { | 313 | host: { |
315 | [ Op.iLike ]: '%' + search + '%' | 314 | [Op.iLike]: '%' + search + '%' |
316 | } | 315 | } |
317 | }) | 316 | }) |
318 | } | 317 | } |
@@ -362,12 +361,12 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
362 | } | 361 | } |
363 | 362 | ||
364 | static listFollowersForApi (options: { | 363 | static listFollowersForApi (options: { |
365 | actorId: number, | 364 | actorId: number |
366 | start: number, | 365 | start: number |
367 | count: number, | 366 | count: number |
368 | sort: string, | 367 | sort: string |
369 | state?: FollowState, | 368 | state?: FollowState |
370 | actorType?: ActivityPubActorType, | 369 | actorType?: ActivityPubActorType |
371 | search?: string | 370 | search?: string |
372 | }) { | 371 | }) { |
373 | const { actorId, start, count, sort, search, state, actorType } = options | 372 | const { actorId, start, count, sort, search, state, actorType } = options |
@@ -379,7 +378,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
379 | if (search) { | 378 | if (search) { |
380 | Object.assign(followerServerWhere, { | 379 | Object.assign(followerServerWhere, { |
381 | host: { | 380 | host: { |
382 | [ Op.iLike ]: '%' + search + '%' | 381 | [Op.iLike]: '%' + search + '%' |
383 | } | 382 | } |
384 | }) | 383 | }) |
385 | } | 384 | } |
@@ -631,7 +630,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
631 | 630 | ||
632 | const tasks: Bluebird<any>[] = [] | 631 | const tasks: Bluebird<any>[] = [] |
633 | 632 | ||
634 | for (let selection of selections) { | 633 | for (const selection of selections) { |
635 | let query = 'SELECT ' + selection + ' FROM "actor" ' + | 634 | let query = 'SELECT ' + selection + ' FROM "actor" ' + |
636 | 'INNER JOIN "actorFollow" ON "actorFollow"."' + firstJoin + '" = "actor"."id" ' + | 635 | 'INNER JOIN "actorFollow" ON "actorFollow"."' + firstJoin + '" = "actor"."id" ' + |
637 | 'INNER JOIN "actor" AS "Follows" ON "actorFollow"."' + secondJoin + '" = "Follows"."id" ' + | 636 | 'INNER JOIN "actor" AS "Follows" ON "actorFollow"."' + secondJoin + '" = "Follows"."id" ' + |
diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts index 007647ced..e547d2c0c 100644 --- a/server/models/activitypub/actor.ts +++ b/server/models/activitypub/actor.ts | |||
@@ -16,7 +16,7 @@ import { | |||
16 | Table, | 16 | Table, |
17 | UpdatedAt | 17 | UpdatedAt |
18 | } from 'sequelize-typescript' | 18 | } from 'sequelize-typescript' |
19 | import { ActivityPubActorType } from '../../../shared/models/activitypub' | 19 | import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub' |
20 | import { Avatar } from '../../../shared/models/avatars/avatar.model' | 20 | import { Avatar } from '../../../shared/models/avatars/avatar.model' |
21 | import { activityPubContextify } from '../../helpers/activitypub' | 21 | import { activityPubContextify } from '../../helpers/activitypub' |
22 | import { | 22 | import { |
@@ -43,11 +43,12 @@ import { | |||
43 | MActorFull, | 43 | MActorFull, |
44 | MActorHost, | 44 | MActorHost, |
45 | MActorServer, | 45 | MActorServer, |
46 | MActorSummaryFormattable, | 46 | MActorSummaryFormattable, MActorUrl, |
47 | MActorWithInboxes | 47 | MActorWithInboxes |
48 | } from '../../typings/models' | 48 | } from '../../typings/models' |
49 | import * as Bluebird from 'bluebird' | 49 | import * as Bluebird from 'bluebird' |
50 | import { Op, Transaction, literal } from 'sequelize' | 50 | import { Op, Transaction, literal } from 'sequelize' |
51 | import { ModelCache } from '@server/models/model-cache' | ||
51 | 52 | ||
52 | enum ScopeNames { | 53 | enum ScopeNames { |
53 | FULL = 'FULL' | 54 | FULL = 'FULL' |
@@ -276,8 +277,6 @@ export class ActorModel extends Model<ActorModel> { | |||
276 | }) | 277 | }) |
277 | VideoChannel: VideoChannelModel | 278 | VideoChannel: VideoChannelModel |
278 | 279 | ||
279 | private static cache: { [ id: string ]: any } = {} | ||
280 | |||
281 | static load (id: number): Bluebird<MActor> { | 280 | static load (id: number): Bluebird<MActor> { |
282 | return ActorModel.unscoped().findByPk(id) | 281 | return ActorModel.unscoped().findByPk(id) |
283 | } | 282 | } |
@@ -334,7 +333,7 @@ export class ActorModel extends Model<ActorModel> { | |||
334 | const query = { | 333 | const query = { |
335 | where: { | 334 | where: { |
336 | followersUrl: { | 335 | followersUrl: { |
337 | [ Op.in ]: followersUrls | 336 | [Op.in]: followersUrls |
338 | } | 337 | } |
339 | }, | 338 | }, |
340 | transaction | 339 | transaction |
@@ -344,28 +343,50 @@ export class ActorModel extends Model<ActorModel> { | |||
344 | } | 343 | } |
345 | 344 | ||
346 | static loadLocalByName (preferredUsername: string, transaction?: Transaction): Bluebird<MActorFull> { | 345 | static loadLocalByName (preferredUsername: string, transaction?: Transaction): Bluebird<MActorFull> { |
347 | // The server actor never change, so we can easily cache it | 346 | const fun = () => { |
348 | if (preferredUsername === SERVER_ACTOR_NAME && ActorModel.cache[preferredUsername]) { | 347 | const query = { |
349 | return Bluebird.resolve(ActorModel.cache[preferredUsername]) | 348 | where: { |
350 | } | 349 | preferredUsername, |
350 | serverId: null | ||
351 | }, | ||
352 | transaction | ||
353 | } | ||
351 | 354 | ||
352 | const query = { | 355 | return ActorModel.scope(ScopeNames.FULL) |
353 | where: { | 356 | .findOne(query) |
354 | preferredUsername, | ||
355 | serverId: null | ||
356 | }, | ||
357 | transaction | ||
358 | } | 357 | } |
359 | 358 | ||
360 | return ActorModel.scope(ScopeNames.FULL) | 359 | return ModelCache.Instance.doCache({ |
361 | .findOne(query) | 360 | cacheType: 'local-actor-name', |
362 | .then(actor => { | 361 | key: preferredUsername, |
363 | if (preferredUsername === SERVER_ACTOR_NAME) { | 362 | // The server actor never change, so we can easily cache it |
364 | ActorModel.cache[ preferredUsername ] = actor | 363 | whitelist: () => preferredUsername === SERVER_ACTOR_NAME, |
365 | } | 364 | fun |
365 | }) | ||
366 | } | ||
366 | 367 | ||
367 | return actor | 368 | static loadLocalUrlByName (preferredUsername: string, transaction?: Transaction): Bluebird<MActorUrl> { |
368 | }) | 369 | const fun = () => { |
370 | const query = { | ||
371 | attributes: [ 'url' ], | ||
372 | where: { | ||
373 | preferredUsername, | ||
374 | serverId: null | ||
375 | }, | ||
376 | transaction | ||
377 | } | ||
378 | |||
379 | return ActorModel.unscoped() | ||
380 | .findOne(query) | ||
381 | } | ||
382 | |||
383 | return ModelCache.Instance.doCache({ | ||
384 | cacheType: 'local-actor-name', | ||
385 | key: preferredUsername, | ||
386 | // The server actor never change, so we can easily cache it | ||
387 | whitelist: () => preferredUsername === SERVER_ACTOR_NAME, | ||
388 | fun | ||
389 | }) | ||
369 | } | 390 | } |
370 | 391 | ||
371 | static loadByNameAndHost (preferredUsername: string, host: string): Bluebird<MActorFull> { | 392 | static loadByNameAndHost (preferredUsername: string, host: string): Bluebird<MActorFull> { |
@@ -441,6 +462,36 @@ export class ActorModel extends Model<ActorModel> { | |||
441 | }, { where, transaction }) | 462 | }, { where, transaction }) |
442 | } | 463 | } |
443 | 464 | ||
465 | static loadAccountActorByVideoId (videoId: number): Bluebird<MActor> { | ||
466 | const query = { | ||
467 | include: [ | ||
468 | { | ||
469 | attributes: [ 'id' ], | ||
470 | model: AccountModel.unscoped(), | ||
471 | required: true, | ||
472 | include: [ | ||
473 | { | ||
474 | attributes: [ 'id', 'accountId' ], | ||
475 | model: VideoChannelModel.unscoped(), | ||
476 | required: true, | ||
477 | include: [ | ||
478 | { | ||
479 | attributes: [ 'id', 'channelId' ], | ||
480 | model: VideoModel.unscoped(), | ||
481 | where: { | ||
482 | id: videoId | ||
483 | } | ||
484 | } | ||
485 | ] | ||
486 | } | ||
487 | ] | ||
488 | } | ||
489 | ] | ||
490 | } | ||
491 | |||
492 | return ActorModel.unscoped().findOne(query) | ||
493 | } | ||
494 | |||
444 | getSharedInbox (this: MActorWithInboxes) { | 495 | getSharedInbox (this: MActorWithInboxes) { |
445 | return this.sharedInboxUrl || this.inboxUrl | 496 | return this.sharedInboxUrl || this.inboxUrl |
446 | } | 497 | } |
@@ -473,9 +524,11 @@ export class ActorModel extends Model<ActorModel> { | |||
473 | } | 524 | } |
474 | 525 | ||
475 | toActivityPubObject (this: MActorAP, name: string) { | 526 | toActivityPubObject (this: MActorAP, name: string) { |
476 | let icon = undefined | 527 | let icon: ActivityIconObject |
528 | |||
477 | if (this.avatarId) { | 529 | if (this.avatarId) { |
478 | const extension = extname(this.Avatar.filename) | 530 | const extension = extname(this.Avatar.filename) |
531 | |||
479 | icon = { | 532 | icon = { |
480 | type: 'Image', | 533 | type: 'Image', |
481 | mediaType: extension === '.png' ? 'image/png' : 'image/jpeg', | 534 | mediaType: extension === '.png' ? 'image/png' : 'image/jpeg', |
diff --git a/server/models/model-cache.ts b/server/models/model-cache.ts new file mode 100644 index 000000000..a87f99aa2 --- /dev/null +++ b/server/models/model-cache.ts | |||
@@ -0,0 +1,91 @@ | |||
1 | import { Model } from 'sequelize-typescript' | ||
2 | import * as Bluebird from 'bluebird' | ||
3 | import { logger } from '@server/helpers/logger' | ||
4 | |||
5 | type ModelCacheType = | ||
6 | 'local-account-name' | ||
7 | | 'local-actor-name' | ||
8 | | 'local-actor-url' | ||
9 | | 'load-video-immutable-id' | ||
10 | | 'load-video-immutable-url' | ||
11 | |||
12 | type DeleteKey = | ||
13 | 'video' | ||
14 | |||
15 | class ModelCache { | ||
16 | |||
17 | private static instance: ModelCache | ||
18 | |||
19 | private readonly localCache: { [id in ModelCacheType]: Map<string, any> } = { | ||
20 | 'local-account-name': new Map(), | ||
21 | 'local-actor-name': new Map(), | ||
22 | 'local-actor-url': new Map(), | ||
23 | 'load-video-immutable-id': new Map(), | ||
24 | 'load-video-immutable-url': new Map() | ||
25 | } | ||
26 | |||
27 | private readonly deleteIds: { | ||
28 | [deleteKey in DeleteKey]: Map<number, { cacheType: ModelCacheType, key: string }[]> | ||
29 | } = { | ||
30 | video: new Map() | ||
31 | } | ||
32 | |||
33 | private constructor () { | ||
34 | } | ||
35 | |||
36 | static get Instance () { | ||
37 | return this.instance || (this.instance = new this()) | ||
38 | } | ||
39 | |||
40 | doCache<T extends Model> (options: { | ||
41 | cacheType: ModelCacheType | ||
42 | key: string | ||
43 | fun: () => Bluebird<T> | ||
44 | whitelist?: () => boolean | ||
45 | deleteKey?: DeleteKey | ||
46 | }) { | ||
47 | const { cacheType, key, fun, whitelist, deleteKey } = options | ||
48 | |||
49 | if (whitelist && whitelist() !== true) return fun() | ||
50 | |||
51 | const cache = this.localCache[cacheType] | ||
52 | |||
53 | if (cache.has(key)) { | ||
54 | logger.debug('Model cache hit for %s -> %s.', cacheType, key) | ||
55 | return Bluebird.resolve<T>(cache.get(key)) | ||
56 | } | ||
57 | |||
58 | return fun().then(m => { | ||
59 | if (!m) return m | ||
60 | |||
61 | if (!whitelist || whitelist()) cache.set(key, m) | ||
62 | |||
63 | if (deleteKey) { | ||
64 | const map = this.deleteIds[deleteKey] | ||
65 | if (!map.has(m.id)) map.set(m.id, []) | ||
66 | |||
67 | const a = map.get(m.id) | ||
68 | a.push({ cacheType, key }) | ||
69 | } | ||
70 | |||
71 | return m | ||
72 | }) | ||
73 | } | ||
74 | |||
75 | invalidateCache (deleteKey: DeleteKey, modelId: number) { | ||
76 | const map = this.deleteIds[deleteKey] | ||
77 | |||
78 | if (!map.has(modelId)) return | ||
79 | |||
80 | for (const toDelete of map.get(modelId)) { | ||
81 | logger.debug('Removing %s -> %d of model cache %s -> %s.', deleteKey, modelId, toDelete.cacheType, toDelete.key) | ||
82 | this.localCache[toDelete.cacheType].delete(toDelete.key) | ||
83 | } | ||
84 | |||
85 | map.delete(modelId) | ||
86 | } | ||
87 | } | ||
88 | |||
89 | export { | ||
90 | ModelCache | ||
91 | } | ||
diff --git a/server/models/oauth/oauth-token.ts b/server/models/oauth/oauth-token.ts index b680be237..d2101ce86 100644 --- a/server/models/oauth/oauth-token.ts +++ b/server/models/oauth/oauth-token.ts | |||
@@ -23,10 +23,10 @@ import { MOAuthTokenUser } from '@server/typings/models/oauth/oauth-token' | |||
23 | 23 | ||
24 | export type OAuthTokenInfo = { | 24 | export type OAuthTokenInfo = { |
25 | refreshToken: string | 25 | refreshToken: string |
26 | refreshTokenExpiresAt: Date, | 26 | refreshTokenExpiresAt: Date |
27 | client: { | 27 | client: { |
28 | id: number | 28 | id: number |
29 | }, | 29 | } |
30 | user: { | 30 | user: { |
31 | id: number | 31 | id: number |
32 | } | 32 | } |
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index 8c9a7eabf..1b63d3818 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts | |||
@@ -13,13 +13,13 @@ import { | |||
13 | UpdatedAt | 13 | UpdatedAt |
14 | } from 'sequelize-typescript' | 14 | } from 'sequelize-typescript' |
15 | import { ActorModel } from '../activitypub/actor' | 15 | import { ActorModel } from '../activitypub/actor' |
16 | import { getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils' | 16 | import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils' |
17 | import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 17 | import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
18 | import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants' | 18 | import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants' |
19 | import { VideoFileModel } from '../video/video-file' | 19 | import { VideoFileModel } from '../video/video-file' |
20 | import { getServerActor } from '../../helpers/utils' | 20 | import { getServerActor } from '../../helpers/utils' |
21 | import { VideoModel } from '../video/video' | 21 | import { VideoModel } from '../video/video' |
22 | import { VideoRedundancyStrategy } from '../../../shared/models/redundancy' | 22 | import { VideoRedundancyStrategy, VideoRedundancyStrategyWithManual } from '../../../shared/models/redundancy' |
23 | import { logger } from '../../helpers/logger' | 23 | import { logger } from '../../helpers/logger' |
24 | import { CacheFileObject, VideoPrivacy } from '../../../shared' | 24 | import { CacheFileObject, VideoPrivacy } from '../../../shared' |
25 | import { VideoChannelModel } from '../video/video-channel' | 25 | import { VideoChannelModel } from '../video/video-channel' |
@@ -27,17 +27,23 @@ import { ServerModel } from '../server/server' | |||
27 | import { sample } from 'lodash' | 27 | import { sample } from 'lodash' |
28 | import { isTestInstance } from '../../helpers/core-utils' | 28 | import { isTestInstance } from '../../helpers/core-utils' |
29 | import * as Bluebird from 'bluebird' | 29 | import * as Bluebird from 'bluebird' |
30 | import { col, FindOptions, fn, literal, Op, Transaction } from 'sequelize' | 30 | import { col, FindOptions, fn, literal, Op, Transaction, WhereOptions } from 'sequelize' |
31 | import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist' | 31 | import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist' |
32 | import { CONFIG } from '../../initializers/config' | 32 | import { CONFIG } from '../../initializers/config' |
33 | import { MVideoRedundancy, MVideoRedundancyAP, MVideoRedundancyVideo } from '@server/typings/models' | 33 | import { MVideoForRedundancyAPI, MVideoRedundancy, MVideoRedundancyAP, MVideoRedundancyVideo } from '@server/typings/models' |
34 | import { VideoRedundanciesTarget } from '@shared/models/redundancy/video-redundancies-filters.model' | ||
35 | import { | ||
36 | FileRedundancyInformation, | ||
37 | StreamingPlaylistRedundancyInformation, | ||
38 | VideoRedundancy | ||
39 | } from '@shared/models/redundancy/video-redundancy.model' | ||
34 | 40 | ||
35 | export enum ScopeNames { | 41 | export enum ScopeNames { |
36 | WITH_VIDEO = 'WITH_VIDEO' | 42 | WITH_VIDEO = 'WITH_VIDEO' |
37 | } | 43 | } |
38 | 44 | ||
39 | @Scopes(() => ({ | 45 | @Scopes(() => ({ |
40 | [ ScopeNames.WITH_VIDEO ]: { | 46 | [ScopeNames.WITH_VIDEO]: { |
41 | include: [ | 47 | include: [ |
42 | { | 48 | { |
43 | model: VideoFileModel, | 49 | model: VideoFileModel, |
@@ -86,7 +92,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
86 | @UpdatedAt | 92 | @UpdatedAt |
87 | updatedAt: Date | 93 | updatedAt: Date |
88 | 94 | ||
89 | @AllowNull(false) | 95 | @AllowNull(true) |
90 | @Column | 96 | @Column |
91 | expiresOn: Date | 97 | expiresOn: Date |
92 | 98 | ||
@@ -161,7 +167,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
161 | logger.info('Removing duplicated video streaming playlist %s.', videoUUID) | 167 | logger.info('Removing duplicated video streaming playlist %s.', videoUUID) |
162 | 168 | ||
163 | videoStreamingPlaylist.Video.removeStreamingPlaylistFiles(videoStreamingPlaylist, true) | 169 | videoStreamingPlaylist.Video.removeStreamingPlaylistFiles(videoStreamingPlaylist, true) |
164 | .catch(err => logger.error('Cannot delete video streaming playlist files of %s.', videoUUID, { err })) | 170 | .catch(err => logger.error('Cannot delete video streaming playlist files of %s.', videoUUID, { err })) |
165 | } | 171 | } |
166 | 172 | ||
167 | return undefined | 173 | return undefined |
@@ -193,6 +199,15 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
193 | return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query) | 199 | return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query) |
194 | } | 200 | } |
195 | 201 | ||
202 | static loadByIdWithVideo (id: number, transaction?: Transaction): Bluebird<MVideoRedundancyVideo> { | ||
203 | const query = { | ||
204 | where: { id }, | ||
205 | transaction | ||
206 | } | ||
207 | |||
208 | return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query) | ||
209 | } | ||
210 | |||
196 | static loadByUrl (url: string, transaction?: Transaction): Bluebird<MVideoRedundancy> { | 211 | static loadByUrl (url: string, transaction?: Transaction): Bluebird<MVideoRedundancy> { |
197 | const query = { | 212 | const query = { |
198 | where: { | 213 | where: { |
@@ -215,12 +230,12 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
215 | }, | 230 | }, |
216 | include: [ | 231 | include: [ |
217 | { | 232 | { |
218 | attributes: [ ], | 233 | attributes: [], |
219 | model: VideoFileModel, | 234 | model: VideoFileModel, |
220 | required: true, | 235 | required: true, |
221 | include: [ | 236 | include: [ |
222 | { | 237 | { |
223 | attributes: [ ], | 238 | attributes: [], |
224 | model: VideoModel, | 239 | model: VideoModel, |
225 | required: true, | 240 | required: true, |
226 | where: { | 241 | where: { |
@@ -233,7 +248,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
233 | } | 248 | } |
234 | 249 | ||
235 | return VideoRedundancyModel.findOne(query) | 250 | return VideoRedundancyModel.findOne(query) |
236 | .then(r => !!r) | 251 | .then(r => !!r) |
237 | } | 252 | } |
238 | 253 | ||
239 | static async getVideoSample (p: Bluebird<VideoModel[]>) { | 254 | static async getVideoSample (p: Bluebird<VideoModel[]>) { |
@@ -295,7 +310,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
295 | where: { | 310 | where: { |
296 | privacy: VideoPrivacy.PUBLIC, | 311 | privacy: VideoPrivacy.PUBLIC, |
297 | views: { | 312 | views: { |
298 | [ Op.gte ]: minViews | 313 | [Op.gte]: minViews |
299 | } | 314 | } |
300 | }, | 315 | }, |
301 | include: [ | 316 | include: [ |
@@ -318,7 +333,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
318 | actorId: actor.id, | 333 | actorId: actor.id, |
319 | strategy, | 334 | strategy, |
320 | createdAt: { | 335 | createdAt: { |
321 | [ Op.lt ]: expiredDate | 336 | [Op.lt]: expiredDate |
322 | } | 337 | } |
323 | } | 338 | } |
324 | } | 339 | } |
@@ -377,7 +392,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
377 | where: { | 392 | where: { |
378 | actorId: actor.id, | 393 | actorId: actor.id, |
379 | expiresOn: { | 394 | expiresOn: { |
380 | [ Op.lt ]: new Date() | 395 | [Op.lt]: new Date() |
381 | } | 396 | } |
382 | } | 397 | } |
383 | } | 398 | } |
@@ -394,7 +409,8 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
394 | [Op.ne]: actor.id | 409 | [Op.ne]: actor.id |
395 | }, | 410 | }, |
396 | expiresOn: { | 411 | expiresOn: { |
397 | [ Op.lt ]: new Date() | 412 | [Op.lt]: new Date(), |
413 | [Op.ne]: null | ||
398 | } | 414 | } |
399 | } | 415 | } |
400 | } | 416 | } |
@@ -447,7 +463,112 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
447 | return VideoRedundancyModel.findAll(query) | 463 | return VideoRedundancyModel.findAll(query) |
448 | } | 464 | } |
449 | 465 | ||
450 | static async getStats (strategy: VideoRedundancyStrategy) { | 466 | static listForApi (options: { |
467 | start: number | ||
468 | count: number | ||
469 | sort: string | ||
470 | target: VideoRedundanciesTarget | ||
471 | strategy?: string | ||
472 | }) { | ||
473 | const { start, count, sort, target, strategy } = options | ||
474 | const redundancyWhere: WhereOptions = {} | ||
475 | const videosWhere: WhereOptions = {} | ||
476 | let redundancySqlSuffix = '' | ||
477 | |||
478 | if (target === 'my-videos') { | ||
479 | Object.assign(videosWhere, { remote: false }) | ||
480 | } else if (target === 'remote-videos') { | ||
481 | Object.assign(videosWhere, { remote: true }) | ||
482 | Object.assign(redundancyWhere, { strategy: { [Op.ne]: null } }) | ||
483 | redundancySqlSuffix = ' AND "videoRedundancy"."strategy" IS NOT NULL' | ||
484 | } | ||
485 | |||
486 | if (strategy) { | ||
487 | Object.assign(redundancyWhere, { strategy: strategy }) | ||
488 | } | ||
489 | |||
490 | const videoFilterWhere = { | ||
491 | [Op.and]: [ | ||
492 | { | ||
493 | [Op.or]: [ | ||
494 | { | ||
495 | id: { | ||
496 | [Op.in]: literal( | ||
497 | '(' + | ||
498 | 'SELECT "videoId" FROM "videoFile" ' + | ||
499 | 'INNER JOIN "videoRedundancy" ON "videoRedundancy"."videoFileId" = "videoFile".id' + | ||
500 | redundancySqlSuffix + | ||
501 | ')' | ||
502 | ) | ||
503 | } | ||
504 | }, | ||
505 | { | ||
506 | id: { | ||
507 | [Op.in]: literal( | ||
508 | '(' + | ||
509 | 'select "videoId" FROM "videoStreamingPlaylist" ' + | ||
510 | 'INNER JOIN "videoRedundancy" ON "videoRedundancy"."videoStreamingPlaylistId" = "videoStreamingPlaylist".id' + | ||
511 | redundancySqlSuffix + | ||
512 | ')' | ||
513 | ) | ||
514 | } | ||
515 | } | ||
516 | ] | ||
517 | }, | ||
518 | |||
519 | videosWhere | ||
520 | ] | ||
521 | } | ||
522 | |||
523 | // /!\ On video model /!\ | ||
524 | const findOptions = { | ||
525 | offset: start, | ||
526 | limit: count, | ||
527 | order: getSort(sort), | ||
528 | include: [ | ||
529 | { | ||
530 | required: false, | ||
531 | model: VideoFileModel.unscoped(), | ||
532 | include: [ | ||
533 | { | ||
534 | model: VideoRedundancyModel.unscoped(), | ||
535 | required: false, | ||
536 | where: redundancyWhere | ||
537 | } | ||
538 | ] | ||
539 | }, | ||
540 | { | ||
541 | required: false, | ||
542 | model: VideoStreamingPlaylistModel.unscoped(), | ||
543 | include: [ | ||
544 | { | ||
545 | model: VideoRedundancyModel.unscoped(), | ||
546 | required: false, | ||
547 | where: redundancyWhere | ||
548 | }, | ||
549 | { | ||
550 | model: VideoFileModel.unscoped(), | ||
551 | required: false | ||
552 | } | ||
553 | ] | ||
554 | } | ||
555 | ], | ||
556 | where: videoFilterWhere | ||
557 | } | ||
558 | |||
559 | // /!\ On video model /!\ | ||
560 | const countOptions = { | ||
561 | where: videoFilterWhere | ||
562 | } | ||
563 | |||
564 | return Promise.all([ | ||
565 | VideoModel.findAll(findOptions), | ||
566 | |||
567 | VideoModel.count(countOptions) | ||
568 | ]).then(([ data, total ]) => ({ total, data })) | ||
569 | } | ||
570 | |||
571 | static async getStats (strategy: VideoRedundancyStrategyWithManual) { | ||
451 | const actor = await getServerActor() | 572 | const actor = await getServerActor() |
452 | 573 | ||
453 | const query: FindOptions = { | 574 | const query: FindOptions = { |
@@ -471,11 +592,58 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
471 | } | 592 | } |
472 | 593 | ||
473 | return VideoRedundancyModel.findOne(query) | 594 | return VideoRedundancyModel.findOne(query) |
474 | .then((r: any) => ({ | 595 | .then((r: any) => ({ |
475 | totalUsed: parseAggregateResult(r.totalUsed), | 596 | totalUsed: parseAggregateResult(r.totalUsed), |
476 | totalVideos: r.totalVideos, | 597 | totalVideos: r.totalVideos, |
477 | totalVideoFiles: r.totalVideoFiles | 598 | totalVideoFiles: r.totalVideoFiles |
478 | })) | 599 | })) |
600 | } | ||
601 | |||
602 | static toFormattedJSONStatic (video: MVideoForRedundancyAPI): VideoRedundancy { | ||
603 | const filesRedundancies: FileRedundancyInformation[] = [] | ||
604 | const streamingPlaylistsRedundancies: StreamingPlaylistRedundancyInformation[] = [] | ||
605 | |||
606 | for (const file of video.VideoFiles) { | ||
607 | for (const redundancy of file.RedundancyVideos) { | ||
608 | filesRedundancies.push({ | ||
609 | id: redundancy.id, | ||
610 | fileUrl: redundancy.fileUrl, | ||
611 | strategy: redundancy.strategy, | ||
612 | createdAt: redundancy.createdAt, | ||
613 | updatedAt: redundancy.updatedAt, | ||
614 | expiresOn: redundancy.expiresOn, | ||
615 | size: file.size | ||
616 | }) | ||
617 | } | ||
618 | } | ||
619 | |||
620 | for (const playlist of video.VideoStreamingPlaylists) { | ||
621 | const size = playlist.VideoFiles.reduce((a, b) => a + b.size, 0) | ||
622 | |||
623 | for (const redundancy of playlist.RedundancyVideos) { | ||
624 | streamingPlaylistsRedundancies.push({ | ||
625 | id: redundancy.id, | ||
626 | fileUrl: redundancy.fileUrl, | ||
627 | strategy: redundancy.strategy, | ||
628 | createdAt: redundancy.createdAt, | ||
629 | updatedAt: redundancy.updatedAt, | ||
630 | expiresOn: redundancy.expiresOn, | ||
631 | size | ||
632 | }) | ||
633 | } | ||
634 | } | ||
635 | |||
636 | return { | ||
637 | id: video.id, | ||
638 | name: video.name, | ||
639 | url: video.url, | ||
640 | uuid: video.uuid, | ||
641 | |||
642 | redundancies: { | ||
643 | files: filesRedundancies, | ||
644 | streamingPlaylists: streamingPlaylistsRedundancies | ||
645 | } | ||
646 | } | ||
479 | } | 647 | } |
480 | 648 | ||
481 | getVideo () { | 649 | getVideo () { |
@@ -494,7 +662,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
494 | id: this.url, | 662 | id: this.url, |
495 | type: 'CacheFile' as 'CacheFile', | 663 | type: 'CacheFile' as 'CacheFile', |
496 | object: this.VideoStreamingPlaylist.Video.url, | 664 | object: this.VideoStreamingPlaylist.Video.url, |
497 | expires: this.expiresOn.toISOString(), | 665 | expires: this.expiresOn ? this.expiresOn.toISOString() : null, |
498 | url: { | 666 | url: { |
499 | type: 'Link', | 667 | type: 'Link', |
500 | mediaType: 'application/x-mpegURL', | 668 | mediaType: 'application/x-mpegURL', |
@@ -507,10 +675,10 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
507 | id: this.url, | 675 | id: this.url, |
508 | type: 'CacheFile' as 'CacheFile', | 676 | type: 'CacheFile' as 'CacheFile', |
509 | object: this.VideoFile.Video.url, | 677 | object: this.VideoFile.Video.url, |
510 | expires: this.expiresOn.toISOString(), | 678 | expires: this.expiresOn ? this.expiresOn.toISOString() : null, |
511 | url: { | 679 | url: { |
512 | type: 'Link', | 680 | type: 'Link', |
513 | mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any, | 681 | mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[this.VideoFile.extname] as any, |
514 | href: this.fileUrl, | 682 | href: this.fileUrl, |
515 | height: this.VideoFile.resolution, | 683 | height: this.VideoFile.resolution, |
516 | size: this.VideoFile.size, | 684 | size: this.VideoFile.size, |
@@ -525,7 +693,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
525 | 693 | ||
526 | const notIn = literal( | 694 | const notIn = literal( |
527 | '(' + | 695 | '(' + |
528 | `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id} AND "videoFileId" IS NOT NULL` + | 696 | `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id} AND "videoFileId" IS NOT NULL` + |
529 | ')' | 697 | ')' |
530 | ) | 698 | ) |
531 | 699 | ||
@@ -535,7 +703,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
535 | required: true, | 703 | required: true, |
536 | where: { | 704 | where: { |
537 | id: { | 705 | id: { |
538 | [ Op.notIn ]: notIn | 706 | [Op.notIn]: notIn |
539 | } | 707 | } |
540 | } | 708 | } |
541 | } | 709 | } |
diff --git a/server/models/server/plugin.ts b/server/models/server/plugin.ts index d094da1f5..95774a467 100644 --- a/server/models/server/plugin.ts +++ b/server/models/server/plugin.ts | |||
@@ -189,10 +189,10 @@ export class PluginModel extends Model<PluginModel> { | |||
189 | } | 189 | } |
190 | 190 | ||
191 | static listForApi (options: { | 191 | static listForApi (options: { |
192 | pluginType?: PluginType, | 192 | pluginType?: PluginType |
193 | uninstalled?: boolean, | 193 | uninstalled?: boolean |
194 | start: number, | 194 | start: number |
195 | count: number, | 195 | count: number |
196 | sort: string | 196 | sort: string |
197 | }) { | 197 | }) { |
198 | const { uninstalled = false } = options | 198 | const { uninstalled = false } = options |
diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts index b88df4fd5..883ae47ab 100644 --- a/server/models/server/server-blocklist.ts +++ b/server/models/server/server-blocklist.ts | |||
@@ -81,7 +81,7 @@ export class ServerBlocklistModel extends Model<ServerBlocklistModel> { | |||
81 | attributes: [ 'accountId', 'id' ], | 81 | attributes: [ 'accountId', 'id' ], |
82 | where: { | 82 | where: { |
83 | accountId: { | 83 | accountId: { |
84 | [Op.in]: accountIds // FIXME: sequelize ANY seems broken | 84 | [Op.in]: accountIds |
85 | }, | 85 | }, |
86 | targetServerId | 86 | targetServerId |
87 | }, | 87 | }, |
diff --git a/server/models/utils.ts b/server/models/utils.ts index f89b80011..f7afb8d4b 100644 --- a/server/models/utils.ts +++ b/server/models/utils.ts | |||
@@ -67,7 +67,7 @@ function getVideoSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): Or | |||
67 | function getBlacklistSort (model: any, value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | 67 | function getBlacklistSort (model: any, value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { |
68 | const [ firstSort ] = getSort(value) | 68 | const [ firstSort ] = getSort(value) |
69 | 69 | ||
70 | if (model) return [ [ literal(`"${model}.${firstSort[ 0 ]}" ${firstSort[ 1 ]}`) ], lastSort ] as any[] // FIXME: typings | 70 | if (model) return [ [ literal(`"${model}.${firstSort[0]}" ${firstSort[1]}`) ], lastSort ] as any[] // FIXME: typings |
71 | return [ firstSort, lastSort ] | 71 | return [ firstSort, lastSort ] |
72 | } | 72 | } |
73 | 73 | ||
@@ -139,7 +139,7 @@ function buildServerIdsFollowedBy (actorId: any) { | |||
139 | 'SELECT "actor"."serverId" FROM "actorFollow" ' + | 139 | 'SELECT "actor"."serverId" FROM "actorFollow" ' + |
140 | 'INNER JOIN "actor" ON actor.id = "actorFollow"."targetActorId" ' + | 140 | 'INNER JOIN "actor" ON actor.id = "actorFollow"."targetActorId" ' + |
141 | 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + | 141 | 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + |
142 | ')' | 142 | ')' |
143 | } | 143 | } |
144 | 144 | ||
145 | function buildWhereIdOrUUID (id: number | string) { | 145 | function buildWhereIdOrUUID (id: number | string) { |
diff --git a/server/models/video/thumbnail.ts b/server/models/video/thumbnail.ts index 3b011b1d2..e396784d2 100644 --- a/server/models/video/thumbnail.ts +++ b/server/models/video/thumbnail.ts | |||
@@ -19,6 +19,8 @@ import { CONFIG } from '../../initializers/config' | |||
19 | import { VideoModel } from './video' | 19 | import { VideoModel } from './video' |
20 | import { VideoPlaylistModel } from './video-playlist' | 20 | import { VideoPlaylistModel } from './video-playlist' |
21 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' | 21 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' |
22 | import { MVideoAccountLight } from '@server/typings/models' | ||
23 | import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub' | ||
22 | 24 | ||
23 | @Table({ | 25 | @Table({ |
24 | tableName: 'thumbnail', | 26 | tableName: 'thumbnail', |
@@ -90,7 +92,7 @@ export class ThumbnailModel extends Model<ThumbnailModel> { | |||
90 | @UpdatedAt | 92 | @UpdatedAt |
91 | updatedAt: Date | 93 | updatedAt: Date |
92 | 94 | ||
93 | private static types: { [ id in ThumbnailType ]: { label: string, directory: string, staticPath: string } } = { | 95 | private static readonly types: { [ id in ThumbnailType ]: { label: string, directory: string, staticPath: string } } = { |
94 | [ThumbnailType.MINIATURE]: { | 96 | [ThumbnailType.MINIATURE]: { |
95 | label: 'miniature', | 97 | label: 'miniature', |
96 | directory: CONFIG.STORAGE.THUMBNAILS_DIR, | 98 | directory: CONFIG.STORAGE.THUMBNAILS_DIR, |
@@ -126,11 +128,14 @@ export class ThumbnailModel extends Model<ThumbnailModel> { | |||
126 | return videoUUID + '.jpg' | 128 | return videoUUID + '.jpg' |
127 | } | 129 | } |
128 | 130 | ||
129 | getFileUrl (isLocal: boolean) { | 131 | getFileUrl (video: MVideoAccountLight) { |
130 | if (isLocal === false) return this.fileUrl | 132 | const staticPath = ThumbnailModel.types[this.type].staticPath + this.filename |
131 | 133 | ||
132 | const staticPath = ThumbnailModel.types[this.type].staticPath | 134 | if (video.isOwned()) return WEBSERVER.URL + staticPath |
133 | return WEBSERVER.URL + staticPath + this.filename | 135 | if (this.fileUrl) return this.fileUrl |
136 | |||
137 | // Fallback if we don't have a file URL | ||
138 | return buildRemoteVideoBaseUrl(video, staticPath) | ||
134 | } | 139 | } |
135 | 140 | ||
136 | getPath () { | 141 | getPath () { |
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts index 3636db18d..da8c1577c 100644 --- a/server/models/video/video-abuse.ts +++ b/server/models/video/video-abuse.ts | |||
@@ -87,9 +87,9 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> { | |||
87 | } | 87 | } |
88 | 88 | ||
89 | static listForApi (parameters: { | 89 | static listForApi (parameters: { |
90 | start: number, | 90 | start: number |
91 | count: number, | 91 | count: number |
92 | sort: string, | 92 | sort: string |
93 | serverAccountId: number | 93 | serverAccountId: number |
94 | user?: MUserAccountId | 94 | user?: MUserAccountId |
95 | }) { | 95 | }) { |
diff --git a/server/models/video/video-caption.ts b/server/models/video/video-caption.ts index eeb2a4afd..59d3e1050 100644 --- a/server/models/video/video-caption.ts +++ b/server/models/video/video-caption.ts | |||
@@ -5,6 +5,7 @@ import { | |||
5 | BelongsTo, | 5 | BelongsTo, |
6 | Column, | 6 | Column, |
7 | CreatedAt, | 7 | CreatedAt, |
8 | DataType, | ||
8 | ForeignKey, | 9 | ForeignKey, |
9 | Is, | 10 | Is, |
10 | Model, | 11 | Model, |
@@ -16,13 +17,14 @@ import { buildWhereIdOrUUID, throwIfNotValid } from '../utils' | |||
16 | import { VideoModel } from './video' | 17 | import { VideoModel } from './video' |
17 | import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions' | 18 | import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions' |
18 | import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model' | 19 | import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model' |
19 | import { LAZY_STATIC_PATHS, VIDEO_LANGUAGES } from '../../initializers/constants' | 20 | import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, VIDEO_LANGUAGES, WEBSERVER } from '../../initializers/constants' |
20 | import { join } from 'path' | 21 | import { join } from 'path' |
21 | import { logger } from '../../helpers/logger' | 22 | import { logger } from '../../helpers/logger' |
22 | import { remove } from 'fs-extra' | 23 | import { remove } from 'fs-extra' |
23 | import { CONFIG } from '../../initializers/config' | 24 | import { CONFIG } from '../../initializers/config' |
24 | import * as Bluebird from 'bluebird' | 25 | import * as Bluebird from 'bluebird' |
25 | import { MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/typings/models' | 26 | import { MVideoAccountLight, MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/typings/models' |
27 | import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub' | ||
26 | 28 | ||
27 | export enum ScopeNames { | 29 | export enum ScopeNames { |
28 | WITH_VIDEO_UUID_AND_REMOTE = 'WITH_VIDEO_UUID_AND_REMOTE' | 30 | WITH_VIDEO_UUID_AND_REMOTE = 'WITH_VIDEO_UUID_AND_REMOTE' |
@@ -64,6 +66,10 @@ export class VideoCaptionModel extends Model<VideoCaptionModel> { | |||
64 | @Column | 66 | @Column |
65 | language: string | 67 | language: string |
66 | 68 | ||
69 | @AllowNull(true) | ||
70 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.COMMONS.URL.max)) | ||
71 | fileUrl: string | ||
72 | |||
67 | @ForeignKey(() => VideoModel) | 73 | @ForeignKey(() => VideoModel) |
68 | @Column | 74 | @Column |
69 | videoId: number | 75 | videoId: number |
@@ -114,13 +120,14 @@ export class VideoCaptionModel extends Model<VideoCaptionModel> { | |||
114 | return VideoCaptionModel.findOne(query) | 120 | return VideoCaptionModel.findOne(query) |
115 | } | 121 | } |
116 | 122 | ||
117 | static insertOrReplaceLanguage (videoId: number, language: string, transaction: Transaction) { | 123 | static insertOrReplaceLanguage (videoId: number, language: string, fileUrl: string, transaction: Transaction) { |
118 | const values = { | 124 | const values = { |
119 | videoId, | 125 | videoId, |
120 | language | 126 | language, |
127 | fileUrl | ||
121 | } | 128 | } |
122 | 129 | ||
123 | return (VideoCaptionModel.upsert<VideoCaptionModel>(values, { transaction, returning: true }) as any) // FIXME: typings | 130 | return VideoCaptionModel.upsert(values, { transaction, returning: true }) |
124 | .then(([ caption ]) => caption) | 131 | .then(([ caption ]) => caption) |
125 | } | 132 | } |
126 | 133 | ||
@@ -175,4 +182,14 @@ export class VideoCaptionModel extends Model<VideoCaptionModel> { | |||
175 | removeCaptionFile (this: MVideoCaptionFormattable) { | 182 | removeCaptionFile (this: MVideoCaptionFormattable) { |
176 | return remove(CONFIG.STORAGE.CAPTIONS_DIR + this.getCaptionName()) | 183 | return remove(CONFIG.STORAGE.CAPTIONS_DIR + this.getCaptionName()) |
177 | } | 184 | } |
185 | |||
186 | getFileUrl (video: MVideoAccountLight) { | ||
187 | if (!this.Video) this.Video = video as VideoModel | ||
188 | |||
189 | if (video.isOwned()) return WEBSERVER.URL + this.getCaptionStaticPath() | ||
190 | if (this.fileUrl) return this.fileUrl | ||
191 | |||
192 | // Fallback if we don't have a file URL | ||
193 | return buildRemoteVideoBaseUrl(video, this.getCaptionStaticPath()) | ||
194 | } | ||
178 | } | 195 | } |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index e10adcb3a..835216671 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -30,7 +30,7 @@ import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttr | |||
30 | import { VideoModel } from './video' | 30 | import { VideoModel } from './video' |
31 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' | 31 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' |
32 | import { ServerModel } from '../server/server' | 32 | import { ServerModel } from '../server/server' |
33 | import { FindOptions, ModelIndexesOptions, Op } from 'sequelize' | 33 | import { FindOptions, Op } from 'sequelize' |
34 | import { AvatarModel } from '../avatar/avatar' | 34 | import { AvatarModel } from '../avatar/avatar' |
35 | import { VideoPlaylistModel } from './video-playlist' | 35 | import { VideoPlaylistModel } from './video-playlist' |
36 | import * as Bluebird from 'bluebird' | 36 | import * as Bluebird from 'bluebird' |
@@ -43,18 +43,6 @@ import { | |||
43 | MChannelSummaryFormattable | 43 | MChannelSummaryFormattable |
44 | } from '../../typings/models/video' | 44 | } from '../../typings/models/video' |
45 | 45 | ||
46 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation | ||
47 | const indexes: ModelIndexesOptions[] = [ | ||
48 | buildTrigramSearchIndex('video_channel_name_trigram', 'name'), | ||
49 | |||
50 | { | ||
51 | fields: [ 'accountId' ] | ||
52 | }, | ||
53 | { | ||
54 | fields: [ 'actorId' ] | ||
55 | } | ||
56 | ] | ||
57 | |||
58 | export enum ScopeNames { | 46 | export enum ScopeNames { |
59 | FOR_API = 'FOR_API', | 47 | FOR_API = 'FOR_API', |
60 | WITH_ACCOUNT = 'WITH_ACCOUNT', | 48 | WITH_ACCOUNT = 'WITH_ACCOUNT', |
@@ -133,7 +121,7 @@ export type SummaryOptions = { | |||
133 | }, | 121 | }, |
134 | { | 122 | { |
135 | serverId: { | 123 | serverId: { |
136 | [ Op.in ]: Sequelize.literal(inQueryInstanceFollow) | 124 | [Op.in]: Sequelize.literal(inQueryInstanceFollow) |
137 | } | 125 | } |
138 | } | 126 | } |
139 | ] | 127 | ] |
@@ -176,7 +164,16 @@ export type SummaryOptions = { | |||
176 | })) | 164 | })) |
177 | @Table({ | 165 | @Table({ |
178 | tableName: 'videoChannel', | 166 | tableName: 'videoChannel', |
179 | indexes | 167 | indexes: [ |
168 | buildTrigramSearchIndex('video_channel_name_trigram', 'name'), | ||
169 | |||
170 | { | ||
171 | fields: [ 'accountId' ] | ||
172 | }, | ||
173 | { | ||
174 | fields: [ 'actorId' ] | ||
175 | } | ||
176 | ] | ||
180 | }) | 177 | }) |
181 | export class VideoChannelModel extends Model<VideoChannelModel> { | 178 | export class VideoChannelModel extends Model<VideoChannelModel> { |
182 | 179 | ||
@@ -351,9 +348,9 @@ export class VideoChannelModel extends Model<VideoChannelModel> { | |||
351 | } | 348 | } |
352 | 349 | ||
353 | static listByAccount (options: { | 350 | static listByAccount (options: { |
354 | accountId: number, | 351 | accountId: number |
355 | start: number, | 352 | start: number |
356 | count: number, | 353 | count: number |
357 | sort: string | 354 | sort: string |
358 | }) { | 355 | }) { |
359 | const query = { | 356 | const query = { |
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index fb4d16b4d..b33c33d5e 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts | |||
@@ -257,10 +257,10 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
257 | } | 257 | } |
258 | 258 | ||
259 | static async listThreadsForApi (parameters: { | 259 | static async listThreadsForApi (parameters: { |
260 | videoId: number, | 260 | videoId: number |
261 | start: number, | 261 | start: number |
262 | count: number, | 262 | count: number |
263 | sort: string, | 263 | sort: string |
264 | user?: MUserAccountId | 264 | user?: MUserAccountId |
265 | }) { | 265 | }) { |
266 | const { videoId, start, count, sort, user } = parameters | 266 | const { videoId, start, count, sort, user } = parameters |
@@ -300,8 +300,8 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
300 | } | 300 | } |
301 | 301 | ||
302 | static async listThreadCommentsForApi (parameters: { | 302 | static async listThreadCommentsForApi (parameters: { |
303 | videoId: number, | 303 | videoId: number |
304 | threadId: number, | 304 | threadId: number |
305 | user?: MUserAccountId | 305 | user?: MUserAccountId |
306 | }) { | 306 | }) { |
307 | const { videoId, threadId, user } = parameters | 307 | const { videoId, threadId, user } = parameters |
@@ -314,7 +314,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
314 | order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ] as Order, | 314 | order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ] as Order, |
315 | where: { | 315 | where: { |
316 | videoId, | 316 | videoId, |
317 | [ Op.or ]: [ | 317 | [Op.or]: [ |
318 | { id: threadId }, | 318 | { id: threadId }, |
319 | { originCommentId: threadId } | 319 | { originCommentId: threadId } |
320 | ], | 320 | ], |
@@ -346,7 +346,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
346 | order: [ [ 'createdAt', order ] ] as Order, | 346 | order: [ [ 'createdAt', order ] ] as Order, |
347 | where: { | 347 | where: { |
348 | id: { | 348 | id: { |
349 | [ Op.in ]: Sequelize.literal('(' + | 349 | [Op.in]: Sequelize.literal('(' + |
350 | 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' + | 350 | 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' + |
351 | `SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ${comment.id} ` + | 351 | `SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ${comment.id} ` + |
352 | 'UNION ' + | 352 | 'UNION ' + |
@@ -355,7 +355,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
355 | ') ' + | 355 | ') ' + |
356 | 'SELECT id FROM children' + | 356 | 'SELECT id FROM children' + |
357 | ')'), | 357 | ')'), |
358 | [ Op.ne ]: comment.id | 358 | [Op.ne]: comment.id |
359 | } | 359 | } |
360 | }, | 360 | }, |
361 | transaction: t | 361 | transaction: t |
@@ -461,7 +461,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
461 | } | 461 | } |
462 | 462 | ||
463 | isDeleted () { | 463 | isDeleted () { |
464 | return null !== this.deletedAt | 464 | return this.deletedAt !== null |
465 | } | 465 | } |
466 | 466 | ||
467 | extractMentions () { | 467 | extractMentions () { |
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts index 67395e5c0..1fa66fd63 100644 --- a/server/models/video/video-format-utils.ts +++ b/server/models/video/video-format-utils.ts | |||
@@ -27,12 +27,13 @@ import { generateMagnetUri } from '@server/helpers/webtorrent' | |||
27 | export type VideoFormattingJSONOptions = { | 27 | export type VideoFormattingJSONOptions = { |
28 | completeDescription?: boolean | 28 | completeDescription?: boolean |
29 | additionalAttributes: { | 29 | additionalAttributes: { |
30 | state?: boolean, | 30 | state?: boolean |
31 | waitTranscoding?: boolean, | 31 | waitTranscoding?: boolean |
32 | scheduledUpdate?: boolean, | 32 | scheduledUpdate?: boolean |
33 | blacklistInfo?: boolean | 33 | blacklistInfo?: boolean |
34 | } | 34 | } |
35 | } | 35 | } |
36 | |||
36 | function videoModelToFormattedJSON (video: MVideoFormattable, options?: VideoFormattingJSONOptions): Video { | 37 | function videoModelToFormattedJSON (video: MVideoFormattable, options?: VideoFormattingJSONOptions): Video { |
37 | const userHistory = isArray(video.UserVideoHistories) ? video.UserVideoHistories[0] : undefined | 38 | const userHistory = isArray(video.UserVideoHistories) ? video.UserVideoHistories[0] : undefined |
38 | 39 | ||
@@ -181,12 +182,10 @@ function videoFilesModelToFormattedJSON ( | |||
181 | ): VideoFile[] { | 182 | ): VideoFile[] { |
182 | return videoFiles | 183 | return videoFiles |
183 | .map(videoFile => { | 184 | .map(videoFile => { |
184 | let resolutionLabel = videoFile.resolution + 'p' | ||
185 | |||
186 | return { | 185 | return { |
187 | resolution: { | 186 | resolution: { |
188 | id: videoFile.resolution, | 187 | id: videoFile.resolution, |
189 | label: resolutionLabel | 188 | label: videoFile.resolution + 'p' |
190 | }, | 189 | }, |
191 | magnetUri: generateMagnetUri(model, videoFile, baseUrlHttp, baseUrlWs), | 190 | magnetUri: generateMagnetUri(model, videoFile, baseUrlHttp, baseUrlWs), |
192 | size: videoFile.size, | 191 | size: videoFile.size, |
@@ -214,7 +213,7 @@ function addVideoFilesInAPAcc ( | |||
214 | for (const file of files) { | 213 | for (const file of files) { |
215 | acc.push({ | 214 | acc.push({ |
216 | type: 'Link', | 215 | type: 'Link', |
217 | mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any, | 216 | mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] as any, |
218 | href: model.getVideoFileUrl(file, baseUrlHttp), | 217 | href: model.getVideoFileUrl(file, baseUrlHttp), |
219 | height: file.resolution, | 218 | height: file.resolution, |
220 | size: file.size, | 219 | size: file.size, |
@@ -282,10 +281,8 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoTorrentObject { | |||
282 | addVideoFilesInAPAcc(url, video, baseUrlHttp, baseUrlWs, video.VideoFiles || []) | 281 | addVideoFilesInAPAcc(url, video, baseUrlHttp, baseUrlWs, video.VideoFiles || []) |
283 | 282 | ||
284 | for (const playlist of (video.VideoStreamingPlaylists || [])) { | 283 | for (const playlist of (video.VideoStreamingPlaylists || [])) { |
285 | let tag: ActivityTagObject[] | 284 | const tag = playlist.p2pMediaLoaderInfohashes |
286 | 285 | .map(i => ({ type: 'Infohash' as 'Infohash', name: i })) as ActivityTagObject[] | |
287 | tag = playlist.p2pMediaLoaderInfohashes | ||
288 | .map(i => ({ type: 'Infohash' as 'Infohash', name: i })) | ||
289 | tag.push({ | 286 | tag.push({ |
290 | type: 'Link', | 287 | type: 'Link', |
291 | name: 'sha256', | 288 | name: 'sha256', |
@@ -308,11 +305,12 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoTorrentObject { | |||
308 | for (const caption of video.VideoCaptions) { | 305 | for (const caption of video.VideoCaptions) { |
309 | subtitleLanguage.push({ | 306 | subtitleLanguage.push({ |
310 | identifier: caption.language, | 307 | identifier: caption.language, |
311 | name: VideoCaptionModel.getLanguageLabel(caption.language) | 308 | name: VideoCaptionModel.getLanguageLabel(caption.language), |
309 | url: caption.getFileUrl(video) | ||
312 | }) | 310 | }) |
313 | } | 311 | } |
314 | 312 | ||
315 | const miniature = video.getMiniature() | 313 | const icons = [ video.getMiniature(), video.getPreview() ] |
316 | 314 | ||
317 | return { | 315 | return { |
318 | type: 'Video' as 'Video', | 316 | type: 'Video' as 'Video', |
@@ -337,13 +335,13 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoTorrentObject { | |||
337 | content: video.getTruncatedDescription(), | 335 | content: video.getTruncatedDescription(), |
338 | support: video.support, | 336 | support: video.support, |
339 | subtitleLanguage, | 337 | subtitleLanguage, |
340 | icon: { | 338 | icon: icons.map(i => ({ |
341 | type: 'Image', | 339 | type: 'Image', |
342 | url: miniature.getFileUrl(video.isOwned()), | 340 | url: i.getFileUrl(video), |
343 | mediaType: 'image/jpeg', | 341 | mediaType: 'image/jpeg', |
344 | width: miniature.width, | 342 | width: i.width, |
345 | height: miniature.height | 343 | height: i.height |
346 | }, | 344 | })), |
347 | url, | 345 | url, |
348 | likes: getVideoLikesActivityPubUrl(video), | 346 | likes: getVideoLikesActivityPubUrl(video), |
349 | dislikes: getVideoDislikesActivityPubUrl(video), | 347 | dislikes: getVideoDislikesActivityPubUrl(video), |
diff --git a/server/models/video/video-playlist-element.ts b/server/models/video/video-playlist-element.ts index f2d71357f..4ba16f5fd 100644 --- a/server/models/video/video-playlist-element.ts +++ b/server/models/video/video-playlist-element.ts | |||
@@ -120,10 +120,10 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel> | |||
120 | } | 120 | } |
121 | 121 | ||
122 | static listForApi (options: { | 122 | static listForApi (options: { |
123 | start: number, | 123 | start: number |
124 | count: number, | 124 | count: number |
125 | videoPlaylistId: number, | 125 | videoPlaylistId: number |
126 | serverAccount: AccountModel, | 126 | serverAccount: AccountModel |
127 | user?: MUserAccountId | 127 | user?: MUserAccountId |
128 | }) { | 128 | }) { |
129 | const accountIds = [ options.serverAccount.id ] | 129 | const accountIds = [ options.serverAccount.id ] |
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index bcdda36e5..4ca17ebec 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts | |||
@@ -68,12 +68,12 @@ type AvailableForListOptions = { | |||
68 | type?: VideoPlaylistType | 68 | type?: VideoPlaylistType |
69 | accountId?: number | 69 | accountId?: number |
70 | videoChannelId?: number | 70 | videoChannelId?: number |
71 | listMyPlaylists?: boolean, | 71 | listMyPlaylists?: boolean |
72 | search?: string | 72 | search?: string |
73 | } | 73 | } |
74 | 74 | ||
75 | @Scopes(() => ({ | 75 | @Scopes(() => ({ |
76 | [ ScopeNames.WITH_THUMBNAIL ]: { | 76 | [ScopeNames.WITH_THUMBNAIL]: { |
77 | include: [ | 77 | include: [ |
78 | { | 78 | { |
79 | model: ThumbnailModel, | 79 | model: ThumbnailModel, |
@@ -81,7 +81,7 @@ type AvailableForListOptions = { | |||
81 | } | 81 | } |
82 | ] | 82 | ] |
83 | }, | 83 | }, |
84 | [ ScopeNames.WITH_VIDEOS_LENGTH ]: { | 84 | [ScopeNames.WITH_VIDEOS_LENGTH]: { |
85 | attributes: { | 85 | attributes: { |
86 | include: [ | 86 | include: [ |
87 | [ | 87 | [ |
@@ -91,7 +91,7 @@ type AvailableForListOptions = { | |||
91 | ] | 91 | ] |
92 | } | 92 | } |
93 | } as FindOptions, | 93 | } as FindOptions, |
94 | [ ScopeNames.WITH_ACCOUNT ]: { | 94 | [ScopeNames.WITH_ACCOUNT]: { |
95 | include: [ | 95 | include: [ |
96 | { | 96 | { |
97 | model: AccountModel, | 97 | model: AccountModel, |
@@ -99,7 +99,7 @@ type AvailableForListOptions = { | |||
99 | } | 99 | } |
100 | ] | 100 | ] |
101 | }, | 101 | }, |
102 | [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY ]: { | 102 | [ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY]: { |
103 | include: [ | 103 | include: [ |
104 | { | 104 | { |
105 | model: AccountModel.scope(AccountScopeNames.SUMMARY), | 105 | model: AccountModel.scope(AccountScopeNames.SUMMARY), |
@@ -111,7 +111,7 @@ type AvailableForListOptions = { | |||
111 | } | 111 | } |
112 | ] | 112 | ] |
113 | }, | 113 | }, |
114 | [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL ]: { | 114 | [ScopeNames.WITH_ACCOUNT_AND_CHANNEL]: { |
115 | include: [ | 115 | include: [ |
116 | { | 116 | { |
117 | model: AccountModel, | 117 | model: AccountModel, |
@@ -123,7 +123,7 @@ type AvailableForListOptions = { | |||
123 | } | 123 | } |
124 | ] | 124 | ] |
125 | }, | 125 | }, |
126 | [ ScopeNames.AVAILABLE_FOR_LIST ]: (options: AvailableForListOptions) => { | 126 | [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => { |
127 | 127 | ||
128 | let whereActor: WhereOptions = {} | 128 | let whereActor: WhereOptions = {} |
129 | 129 | ||
@@ -138,13 +138,13 @@ type AvailableForListOptions = { | |||
138 | const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId) | 138 | const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId) |
139 | 139 | ||
140 | whereActor = { | 140 | whereActor = { |
141 | [ Op.or ]: [ | 141 | [Op.or]: [ |
142 | { | 142 | { |
143 | serverId: null | 143 | serverId: null |
144 | }, | 144 | }, |
145 | { | 145 | { |
146 | serverId: { | 146 | serverId: { |
147 | [ Op.in ]: literal(inQueryInstanceFollow) | 147 | [Op.in]: literal(inQueryInstanceFollow) |
148 | } | 148 | } |
149 | } | 149 | } |
150 | ] | 150 | ] |
@@ -172,7 +172,7 @@ type AvailableForListOptions = { | |||
172 | if (options.search) { | 172 | if (options.search) { |
173 | whereAnd.push({ | 173 | whereAnd.push({ |
174 | name: { | 174 | name: { |
175 | [ Op.iLike ]: '%' + options.search + '%' | 175 | [Op.iLike]: '%' + options.search + '%' |
176 | } | 176 | } |
177 | }) | 177 | }) |
178 | } | 178 | } |
@@ -299,13 +299,13 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
299 | 299 | ||
300 | static listForApi (options: { | 300 | static listForApi (options: { |
301 | followerActorId: number | 301 | followerActorId: number |
302 | start: number, | 302 | start: number |
303 | count: number, | 303 | count: number |
304 | sort: string, | 304 | sort: string |
305 | type?: VideoPlaylistType, | 305 | type?: VideoPlaylistType |
306 | accountId?: number, | 306 | accountId?: number |
307 | videoChannelId?: number, | 307 | videoChannelId?: number |
308 | listMyPlaylists?: boolean, | 308 | listMyPlaylists?: boolean |
309 | search?: string | 309 | search?: string |
310 | }) { | 310 | }) { |
311 | const query = { | 311 | const query = { |
@@ -369,7 +369,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
369 | model: VideoPlaylistElementModel.unscoped(), | 369 | model: VideoPlaylistElementModel.unscoped(), |
370 | where: { | 370 | where: { |
371 | videoId: { | 371 | videoId: { |
372 | [Op.in]: videoIds // FIXME: sequelize ANY seems broken | 372 | [Op.in]: videoIds |
373 | } | 373 | } |
374 | }, | 374 | }, |
375 | required: true | 375 | required: true |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index a91a7663d..2e518317d 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -1,18 +1,7 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { maxBy, minBy } from 'lodash' | 2 | import { maxBy, minBy } from 'lodash' |
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import { | 4 | import { CountOptions, FindOptions, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' |
5 | CountOptions, | ||
6 | FindOptions, | ||
7 | IncludeOptions, | ||
8 | ModelIndexesOptions, | ||
9 | Op, | ||
10 | QueryTypes, | ||
11 | ScopeOptions, | ||
12 | Sequelize, | ||
13 | Transaction, | ||
14 | WhereOptions | ||
15 | } from 'sequelize' | ||
16 | import { | 5 | import { |
17 | AllowNull, | 6 | AllowNull, |
18 | BeforeDestroy, | 7 | BeforeDestroy, |
@@ -131,7 +120,7 @@ import { | |||
131 | MVideoFormattableDetails, | 120 | MVideoFormattableDetails, |
132 | MVideoForUser, | 121 | MVideoForUser, |
133 | MVideoFullLight, | 122 | MVideoFullLight, |
134 | MVideoIdThumbnail, | 123 | MVideoIdThumbnail, MVideoImmutable, |
135 | MVideoThumbnail, | 124 | MVideoThumbnail, |
136 | MVideoThumbnailBlacklist, | 125 | MVideoThumbnailBlacklist, |
137 | MVideoWithAllFiles, | 126 | MVideoWithAllFiles, |
@@ -143,74 +132,7 @@ import { MThumbnail } from '../../typings/models/video/thumbnail' | |||
143 | import { VideoFile } from '@shared/models/videos/video-file.model' | 132 | import { VideoFile } from '@shared/models/videos/video-file.model' |
144 | import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' | 133 | import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' |
145 | import validator from 'validator' | 134 | import validator from 'validator' |
146 | 135 | import { ModelCache } from '@server/models/model-cache' | |
147 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation | ||
148 | const indexes: (ModelIndexesOptions & { where?: WhereOptions })[] = [ | ||
149 | buildTrigramSearchIndex('video_name_trigram', 'name'), | ||
150 | |||
151 | { fields: [ 'createdAt' ] }, | ||
152 | { | ||
153 | fields: [ | ||
154 | { name: 'publishedAt', order: 'DESC' }, | ||
155 | { name: 'id', order: 'ASC' } | ||
156 | ] | ||
157 | }, | ||
158 | { fields: [ 'duration' ] }, | ||
159 | { fields: [ 'views' ] }, | ||
160 | { fields: [ 'channelId' ] }, | ||
161 | { | ||
162 | fields: [ 'originallyPublishedAt' ], | ||
163 | where: { | ||
164 | originallyPublishedAt: { | ||
165 | [Op.ne]: null | ||
166 | } | ||
167 | } | ||
168 | }, | ||
169 | { | ||
170 | fields: [ 'category' ], // We don't care videos with an unknown category | ||
171 | where: { | ||
172 | category: { | ||
173 | [Op.ne]: null | ||
174 | } | ||
175 | } | ||
176 | }, | ||
177 | { | ||
178 | fields: [ 'licence' ], // We don't care videos with an unknown licence | ||
179 | where: { | ||
180 | licence: { | ||
181 | [Op.ne]: null | ||
182 | } | ||
183 | } | ||
184 | }, | ||
185 | { | ||
186 | fields: [ 'language' ], // We don't care videos with an unknown language | ||
187 | where: { | ||
188 | language: { | ||
189 | [Op.ne]: null | ||
190 | } | ||
191 | } | ||
192 | }, | ||
193 | { | ||
194 | fields: [ 'nsfw' ], // Most of the videos are not NSFW | ||
195 | where: { | ||
196 | nsfw: true | ||
197 | } | ||
198 | }, | ||
199 | { | ||
200 | fields: [ 'remote' ], // Only index local videos | ||
201 | where: { | ||
202 | remote: false | ||
203 | } | ||
204 | }, | ||
205 | { | ||
206 | fields: [ 'uuid' ], | ||
207 | unique: true | ||
208 | }, | ||
209 | { | ||
210 | fields: [ 'url' ], | ||
211 | unique: true | ||
212 | } | ||
213 | ] | ||
214 | 136 | ||
215 | export enum ScopeNames { | 137 | export enum ScopeNames { |
216 | AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', | 138 | AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', |
@@ -223,6 +145,7 @@ export enum ScopeNames { | |||
223 | WITH_USER_HISTORY = 'WITH_USER_HISTORY', | 145 | WITH_USER_HISTORY = 'WITH_USER_HISTORY', |
224 | WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', | 146 | WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', |
225 | WITH_USER_ID = 'WITH_USER_ID', | 147 | WITH_USER_ID = 'WITH_USER_ID', |
148 | WITH_IMMUTABLE_ATTRIBUTES = 'WITH_IMMUTABLE_ATTRIBUTES', | ||
226 | WITH_THUMBNAILS = 'WITH_THUMBNAILS' | 149 | WITH_THUMBNAILS = 'WITH_THUMBNAILS' |
227 | } | 150 | } |
228 | 151 | ||
@@ -266,7 +189,10 @@ export type AvailableForListIDsOptions = { | |||
266 | } | 189 | } |
267 | 190 | ||
268 | @Scopes(() => ({ | 191 | @Scopes(() => ({ |
269 | [ ScopeNames.FOR_API ]: (options: ForAPIOptions) => { | 192 | [ScopeNames.WITH_IMMUTABLE_ATTRIBUTES]: { |
193 | attributes: [ 'id', 'url', 'uuid', 'remote' ] | ||
194 | }, | ||
195 | [ScopeNames.FOR_API]: (options: ForAPIOptions) => { | ||
270 | const query: FindOptions = { | 196 | const query: FindOptions = { |
271 | include: [ | 197 | include: [ |
272 | { | 198 | { |
@@ -291,7 +217,7 @@ export type AvailableForListIDsOptions = { | |||
291 | if (options.ids) { | 217 | if (options.ids) { |
292 | query.where = { | 218 | query.where = { |
293 | id: { | 219 | id: { |
294 | [ Op.in ]: options.ids // FIXME: sequelize ANY seems broken | 220 | [Op.in]: options.ids |
295 | } | 221 | } |
296 | } | 222 | } |
297 | } | 223 | } |
@@ -315,7 +241,7 @@ export type AvailableForListIDsOptions = { | |||
315 | 241 | ||
316 | return query | 242 | return query |
317 | }, | 243 | }, |
318 | [ ScopeNames.AVAILABLE_FOR_LIST_IDS ]: (options: AvailableForListIDsOptions) => { | 244 | [ScopeNames.AVAILABLE_FOR_LIST_IDS]: (options: AvailableForListIDsOptions) => { |
319 | const whereAnd = options.baseWhere ? [].concat(options.baseWhere) : [] | 245 | const whereAnd = options.baseWhere ? [].concat(options.baseWhere) : [] |
320 | 246 | ||
321 | const query: FindOptions = { | 247 | const query: FindOptions = { |
@@ -326,11 +252,11 @@ export type AvailableForListIDsOptions = { | |||
326 | const attributesType = options.attributesType || 'id' | 252 | const attributesType = options.attributesType || 'id' |
327 | 253 | ||
328 | if (attributesType === 'id') query.attributes = [ 'id' ] | 254 | if (attributesType === 'id') query.attributes = [ 'id' ] |
329 | else if (attributesType === 'none') query.attributes = [ ] | 255 | else if (attributesType === 'none') query.attributes = [] |
330 | 256 | ||
331 | whereAnd.push({ | 257 | whereAnd.push({ |
332 | id: { | 258 | id: { |
333 | [ Op.notIn ]: Sequelize.literal( | 259 | [Op.notIn]: Sequelize.literal( |
334 | '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")' | 260 | '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")' |
335 | ) | 261 | ) |
336 | } | 262 | } |
@@ -339,7 +265,7 @@ export type AvailableForListIDsOptions = { | |||
339 | if (options.serverAccountId) { | 265 | if (options.serverAccountId) { |
340 | whereAnd.push({ | 266 | whereAnd.push({ |
341 | channelId: { | 267 | channelId: { |
342 | [ Op.notIn ]: Sequelize.literal( | 268 | [Op.notIn]: Sequelize.literal( |
343 | '(' + | 269 | '(' + |
344 | 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' + | 270 | 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' + |
345 | buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) + | 271 | buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) + |
@@ -352,15 +278,14 @@ export type AvailableForListIDsOptions = { | |||
352 | 278 | ||
353 | // Only list public/published videos | 279 | // Only list public/published videos |
354 | if (!options.filter || options.filter !== 'all-local') { | 280 | if (!options.filter || options.filter !== 'all-local') { |
355 | |||
356 | const publishWhere = { | 281 | const publishWhere = { |
357 | // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding | 282 | // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding |
358 | [ Op.or ]: [ | 283 | [Op.or]: [ |
359 | { | 284 | { |
360 | state: VideoState.PUBLISHED | 285 | state: VideoState.PUBLISHED |
361 | }, | 286 | }, |
362 | { | 287 | { |
363 | [ Op.and ]: { | 288 | [Op.and]: { |
364 | state: VideoState.TO_TRANSCODE, | 289 | state: VideoState.TO_TRANSCODE, |
365 | waitTranscoding: false | 290 | waitTranscoding: false |
366 | } | 291 | } |
@@ -467,7 +392,7 @@ export type AvailableForListIDsOptions = { | |||
467 | if (options.withFiles === true) { | 392 | if (options.withFiles === true) { |
468 | whereAnd.push({ | 393 | whereAnd.push({ |
469 | id: { | 394 | id: { |
470 | [ Op.in ]: Sequelize.literal( | 395 | [Op.in]: Sequelize.literal( |
471 | '(SELECT "videoId" FROM "videoFile")' | 396 | '(SELECT "videoId" FROM "videoFile")' |
472 | ) | 397 | ) |
473 | } | 398 | } |
@@ -481,7 +406,7 @@ export type AvailableForListIDsOptions = { | |||
481 | 406 | ||
482 | whereAnd.push({ | 407 | whereAnd.push({ |
483 | id: { | 408 | id: { |
484 | [ Op.in ]: Sequelize.literal( | 409 | [Op.in]: Sequelize.literal( |
485 | '(' + | 410 | '(' + |
486 | 'SELECT "videoId" FROM "videoTag" ' + | 411 | 'SELECT "videoId" FROM "videoTag" ' + |
487 | 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + | 412 | 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + |
@@ -497,7 +422,7 @@ export type AvailableForListIDsOptions = { | |||
497 | 422 | ||
498 | whereAnd.push({ | 423 | whereAnd.push({ |
499 | id: { | 424 | id: { |
500 | [ Op.in ]: Sequelize.literal( | 425 | [Op.in]: Sequelize.literal( |
501 | '(' + | 426 | '(' + |
502 | 'SELECT "videoId" FROM "videoTag" ' + | 427 | 'SELECT "videoId" FROM "videoTag" ' + |
503 | 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + | 428 | 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + |
@@ -517,7 +442,7 @@ export type AvailableForListIDsOptions = { | |||
517 | if (options.categoryOneOf) { | 442 | if (options.categoryOneOf) { |
518 | whereAnd.push({ | 443 | whereAnd.push({ |
519 | category: { | 444 | category: { |
520 | [ Op.or ]: options.categoryOneOf | 445 | [Op.or]: options.categoryOneOf |
521 | } | 446 | } |
522 | }) | 447 | }) |
523 | } | 448 | } |
@@ -525,7 +450,7 @@ export type AvailableForListIDsOptions = { | |||
525 | if (options.licenceOneOf) { | 450 | if (options.licenceOneOf) { |
526 | whereAnd.push({ | 451 | whereAnd.push({ |
527 | licence: { | 452 | licence: { |
528 | [ Op.or ]: options.licenceOneOf | 453 | [Op.or]: options.licenceOneOf |
529 | } | 454 | } |
530 | }) | 455 | }) |
531 | } | 456 | } |
@@ -540,12 +465,12 @@ export type AvailableForListIDsOptions = { | |||
540 | [Op.or]: [ | 465 | [Op.or]: [ |
541 | { | 466 | { |
542 | language: { | 467 | language: { |
543 | [ Op.or ]: videoLanguages | 468 | [Op.or]: videoLanguages |
544 | } | 469 | } |
545 | }, | 470 | }, |
546 | { | 471 | { |
547 | id: { | 472 | id: { |
548 | [ Op.in ]: Sequelize.literal( | 473 | [Op.in]: Sequelize.literal( |
549 | '(' + | 474 | '(' + |
550 | 'SELECT "videoId" FROM "videoCaption" ' + | 475 | 'SELECT "videoId" FROM "videoCaption" ' + |
551 | 'WHERE "language" IN (' + createSafeIn(VideoModel, options.languageOneOf) + ') ' + | 476 | 'WHERE "language" IN (' + createSafeIn(VideoModel, options.languageOneOf) + ') ' + |
@@ -579,12 +504,12 @@ export type AvailableForListIDsOptions = { | |||
579 | } | 504 | } |
580 | 505 | ||
581 | query.where = { | 506 | query.where = { |
582 | [ Op.and ]: whereAnd | 507 | [Op.and]: whereAnd |
583 | } | 508 | } |
584 | 509 | ||
585 | return query | 510 | return query |
586 | }, | 511 | }, |
587 | [ ScopeNames.WITH_THUMBNAILS ]: { | 512 | [ScopeNames.WITH_THUMBNAILS]: { |
588 | include: [ | 513 | include: [ |
589 | { | 514 | { |
590 | model: ThumbnailModel, | 515 | model: ThumbnailModel, |
@@ -592,7 +517,7 @@ export type AvailableForListIDsOptions = { | |||
592 | } | 517 | } |
593 | ] | 518 | ] |
594 | }, | 519 | }, |
595 | [ ScopeNames.WITH_USER_ID ]: { | 520 | [ScopeNames.WITH_USER_ID]: { |
596 | include: [ | 521 | include: [ |
597 | { | 522 | { |
598 | attributes: [ 'accountId' ], | 523 | attributes: [ 'accountId' ], |
@@ -608,7 +533,7 @@ export type AvailableForListIDsOptions = { | |||
608 | } | 533 | } |
609 | ] | 534 | ] |
610 | }, | 535 | }, |
611 | [ ScopeNames.WITH_ACCOUNT_DETAILS ]: { | 536 | [ScopeNames.WITH_ACCOUNT_DETAILS]: { |
612 | include: [ | 537 | include: [ |
613 | { | 538 | { |
614 | model: VideoChannelModel.unscoped(), | 539 | model: VideoChannelModel.unscoped(), |
@@ -660,10 +585,10 @@ export type AvailableForListIDsOptions = { | |||
660 | } | 585 | } |
661 | ] | 586 | ] |
662 | }, | 587 | }, |
663 | [ ScopeNames.WITH_TAGS ]: { | 588 | [ScopeNames.WITH_TAGS]: { |
664 | include: [ TagModel ] | 589 | include: [ TagModel ] |
665 | }, | 590 | }, |
666 | [ ScopeNames.WITH_BLACKLISTED ]: { | 591 | [ScopeNames.WITH_BLACKLISTED]: { |
667 | include: [ | 592 | include: [ |
668 | { | 593 | { |
669 | attributes: [ 'id', 'reason', 'unfederated' ], | 594 | attributes: [ 'id', 'reason', 'unfederated' ], |
@@ -672,7 +597,7 @@ export type AvailableForListIDsOptions = { | |||
672 | } | 597 | } |
673 | ] | 598 | ] |
674 | }, | 599 | }, |
675 | [ ScopeNames.WITH_WEBTORRENT_FILES ]: (withRedundancies = false) => { | 600 | [ScopeNames.WITH_WEBTORRENT_FILES]: (withRedundancies = false) => { |
676 | let subInclude: any[] = [] | 601 | let subInclude: any[] = [] |
677 | 602 | ||
678 | if (withRedundancies === true) { | 603 | if (withRedundancies === true) { |
@@ -696,7 +621,7 @@ export type AvailableForListIDsOptions = { | |||
696 | ] | 621 | ] |
697 | } | 622 | } |
698 | }, | 623 | }, |
699 | [ ScopeNames.WITH_STREAMING_PLAYLISTS ]: (withRedundancies = false) => { | 624 | [ScopeNames.WITH_STREAMING_PLAYLISTS]: (withRedundancies = false) => { |
700 | const subInclude: IncludeOptions[] = [ | 625 | const subInclude: IncludeOptions[] = [ |
701 | { | 626 | { |
702 | model: VideoFileModel.unscoped(), | 627 | model: VideoFileModel.unscoped(), |
@@ -723,7 +648,7 @@ export type AvailableForListIDsOptions = { | |||
723 | ] | 648 | ] |
724 | } | 649 | } |
725 | }, | 650 | }, |
726 | [ ScopeNames.WITH_SCHEDULED_UPDATE ]: { | 651 | [ScopeNames.WITH_SCHEDULED_UPDATE]: { |
727 | include: [ | 652 | include: [ |
728 | { | 653 | { |
729 | model: ScheduleVideoUpdateModel.unscoped(), | 654 | model: ScheduleVideoUpdateModel.unscoped(), |
@@ -731,7 +656,7 @@ export type AvailableForListIDsOptions = { | |||
731 | } | 656 | } |
732 | ] | 657 | ] |
733 | }, | 658 | }, |
734 | [ ScopeNames.WITH_USER_HISTORY ]: (userId: number) => { | 659 | [ScopeNames.WITH_USER_HISTORY]: (userId: number) => { |
735 | return { | 660 | return { |
736 | include: [ | 661 | include: [ |
737 | { | 662 | { |
@@ -748,7 +673,72 @@ export type AvailableForListIDsOptions = { | |||
748 | })) | 673 | })) |
749 | @Table({ | 674 | @Table({ |
750 | tableName: 'video', | 675 | tableName: 'video', |
751 | indexes | 676 | indexes: [ |
677 | buildTrigramSearchIndex('video_name_trigram', 'name'), | ||
678 | |||
679 | { fields: [ 'createdAt' ] }, | ||
680 | { | ||
681 | fields: [ | ||
682 | { name: 'publishedAt', order: 'DESC' }, | ||
683 | { name: 'id', order: 'ASC' } | ||
684 | ] | ||
685 | }, | ||
686 | { fields: [ 'duration' ] }, | ||
687 | { fields: [ 'views' ] }, | ||
688 | { fields: [ 'channelId' ] }, | ||
689 | { | ||
690 | fields: [ 'originallyPublishedAt' ], | ||
691 | where: { | ||
692 | originallyPublishedAt: { | ||
693 | [Op.ne]: null | ||
694 | } | ||
695 | } | ||
696 | }, | ||
697 | { | ||
698 | fields: [ 'category' ], // We don't care videos with an unknown category | ||
699 | where: { | ||
700 | category: { | ||
701 | [Op.ne]: null | ||
702 | } | ||
703 | } | ||
704 | }, | ||
705 | { | ||
706 | fields: [ 'licence' ], // We don't care videos with an unknown licence | ||
707 | where: { | ||
708 | licence: { | ||
709 | [Op.ne]: null | ||
710 | } | ||
711 | } | ||
712 | }, | ||
713 | { | ||
714 | fields: [ 'language' ], // We don't care videos with an unknown language | ||
715 | where: { | ||
716 | language: { | ||
717 | [Op.ne]: null | ||
718 | } | ||
719 | } | ||
720 | }, | ||
721 | { | ||
722 | fields: [ 'nsfw' ], // Most of the videos are not NSFW | ||
723 | where: { | ||
724 | nsfw: true | ||
725 | } | ||
726 | }, | ||
727 | { | ||
728 | fields: [ 'remote' ], // Only index local videos | ||
729 | where: { | ||
730 | remote: false | ||
731 | } | ||
732 | }, | ||
733 | { | ||
734 | fields: [ 'uuid' ], | ||
735 | unique: true | ||
736 | }, | ||
737 | { | ||
738 | fields: [ 'url' ], | ||
739 | unique: true | ||
740 | } | ||
741 | ] | ||
752 | }) | 742 | }) |
753 | export class VideoModel extends Model<VideoModel> { | 743 | export class VideoModel extends Model<VideoModel> { |
754 | 744 | ||
@@ -1019,7 +1009,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1019 | }, | 1009 | }, |
1020 | onDelete: 'cascade', | 1010 | onDelete: 'cascade', |
1021 | hooks: true, | 1011 | hooks: true, |
1022 | [ 'separate' as any ]: true | 1012 | ['separate' as any]: true |
1023 | }) | 1013 | }) |
1024 | VideoCaptions: VideoCaptionModel[] | 1014 | VideoCaptions: VideoCaptionModel[] |
1025 | 1015 | ||
@@ -1078,6 +1068,11 @@ export class VideoModel extends Model<VideoModel> { | |||
1078 | return undefined | 1068 | return undefined |
1079 | } | 1069 | } |
1080 | 1070 | ||
1071 | @BeforeDestroy | ||
1072 | static invalidateCache (instance: VideoModel) { | ||
1073 | ModelCache.Instance.invalidateCache('video', instance.id) | ||
1074 | } | ||
1075 | |||
1081 | static listLocal (): Bluebird<MVideoWithAllFiles[]> { | 1076 | static listLocal (): Bluebird<MVideoWithAllFiles[]> { |
1082 | const query = { | 1077 | const query = { |
1083 | where: { | 1078 | where: { |
@@ -1115,16 +1110,16 @@ export class VideoModel extends Model<VideoModel> { | |||
1115 | order: getVideoSort('createdAt', [ 'Tags', 'name', 'ASC' ] as any), // FIXME: sequelize typings | 1110 | order: getVideoSort('createdAt', [ 'Tags', 'name', 'ASC' ] as any), // FIXME: sequelize typings |
1116 | where: { | 1111 | where: { |
1117 | id: { | 1112 | id: { |
1118 | [ Op.in ]: Sequelize.literal('(' + rawQuery + ')') | 1113 | [Op.in]: Sequelize.literal('(' + rawQuery + ')') |
1119 | }, | 1114 | }, |
1120 | [ Op.or ]: [ | 1115 | [Op.or]: [ |
1121 | { privacy: VideoPrivacy.PUBLIC }, | 1116 | { privacy: VideoPrivacy.PUBLIC }, |
1122 | { privacy: VideoPrivacy.UNLISTED } | 1117 | { privacy: VideoPrivacy.UNLISTED } |
1123 | ] | 1118 | ] |
1124 | }, | 1119 | }, |
1125 | include: [ | 1120 | include: [ |
1126 | { | 1121 | { |
1127 | attributes: [ 'language' ], | 1122 | attributes: [ 'language', 'fileUrl' ], |
1128 | model: VideoCaptionModel.unscoped(), | 1123 | model: VideoCaptionModel.unscoped(), |
1129 | required: false | 1124 | required: false |
1130 | }, | 1125 | }, |
@@ -1134,10 +1129,10 @@ export class VideoModel extends Model<VideoModel> { | |||
1134 | required: false, | 1129 | required: false, |
1135 | // We only want videos shared by this actor | 1130 | // We only want videos shared by this actor |
1136 | where: { | 1131 | where: { |
1137 | [ Op.and ]: [ | 1132 | [Op.and]: [ |
1138 | { | 1133 | { |
1139 | id: { | 1134 | id: { |
1140 | [ Op.not ]: null | 1135 | [Op.not]: null |
1141 | } | 1136 | } |
1142 | }, | 1137 | }, |
1143 | { | 1138 | { |
@@ -1187,8 +1182,8 @@ export class VideoModel extends Model<VideoModel> { | |||
1187 | // totals: totalVideos + totalVideoShares | 1182 | // totals: totalVideos + totalVideoShares |
1188 | let totalVideos = 0 | 1183 | let totalVideos = 0 |
1189 | let totalVideoShares = 0 | 1184 | let totalVideoShares = 0 |
1190 | if (totals[ 0 ]) totalVideos = parseInt(totals[ 0 ].total, 10) | 1185 | if (totals[0]) totalVideos = parseInt(totals[0].total, 10) |
1191 | if (totals[ 1 ]) totalVideoShares = parseInt(totals[ 1 ].total, 10) | 1186 | if (totals[1]) totalVideoShares = parseInt(totals[1].total, 10) |
1192 | 1187 | ||
1193 | const total = totalVideos + totalVideoShares | 1188 | const total = totalVideos + totalVideoShares |
1194 | return { | 1189 | return { |
@@ -1231,7 +1226,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1231 | baseQuery = Object.assign(baseQuery, { | 1226 | baseQuery = Object.assign(baseQuery, { |
1232 | where: { | 1227 | where: { |
1233 | name: { | 1228 | name: { |
1234 | [ Op.iLike ]: '%' + search + '%' | 1229 | [Op.iLike]: '%' + search + '%' |
1235 | } | 1230 | } |
1236 | } | 1231 | } |
1237 | }) | 1232 | }) |
@@ -1261,25 +1256,25 @@ export class VideoModel extends Model<VideoModel> { | |||
1261 | } | 1256 | } |
1262 | 1257 | ||
1263 | static async listForApi (options: { | 1258 | static async listForApi (options: { |
1264 | start: number, | 1259 | start: number |
1265 | count: number, | 1260 | count: number |
1266 | sort: string, | 1261 | sort: string |
1267 | nsfw: boolean, | 1262 | nsfw: boolean |
1268 | includeLocalVideos: boolean, | 1263 | includeLocalVideos: boolean |
1269 | withFiles: boolean, | 1264 | withFiles: boolean |
1270 | categoryOneOf?: number[], | 1265 | categoryOneOf?: number[] |
1271 | licenceOneOf?: number[], | 1266 | licenceOneOf?: number[] |
1272 | languageOneOf?: string[], | 1267 | languageOneOf?: string[] |
1273 | tagsOneOf?: string[], | 1268 | tagsOneOf?: string[] |
1274 | tagsAllOf?: string[], | 1269 | tagsAllOf?: string[] |
1275 | filter?: VideoFilter, | 1270 | filter?: VideoFilter |
1276 | accountId?: number, | 1271 | accountId?: number |
1277 | videoChannelId?: number, | 1272 | videoChannelId?: number |
1278 | followerActorId?: number | 1273 | followerActorId?: number |
1279 | videoPlaylistId?: number, | 1274 | videoPlaylistId?: number |
1280 | trendingDays?: number, | 1275 | trendingDays?: number |
1281 | user?: MUserAccountId, | 1276 | user?: MUserAccountId |
1282 | historyOfUser?: MUserId, | 1277 | historyOfUser?: MUserId |
1283 | countVideos?: boolean | 1278 | countVideos?: boolean |
1284 | }) { | 1279 | }) { |
1285 | if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { | 1280 | if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { |
@@ -1345,7 +1340,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1345 | tagsAllOf?: string[] | 1340 | tagsAllOf?: string[] |
1346 | durationMin?: number // seconds | 1341 | durationMin?: number // seconds |
1347 | durationMax?: number // seconds | 1342 | durationMax?: number // seconds |
1348 | user?: MUserAccountId, | 1343 | user?: MUserAccountId |
1349 | filter?: VideoFilter | 1344 | filter?: VideoFilter |
1350 | }) { | 1345 | }) { |
1351 | const whereAnd = [] | 1346 | const whereAnd = [] |
@@ -1353,8 +1348,8 @@ export class VideoModel extends Model<VideoModel> { | |||
1353 | if (options.startDate || options.endDate) { | 1348 | if (options.startDate || options.endDate) { |
1354 | const publishedAtRange = {} | 1349 | const publishedAtRange = {} |
1355 | 1350 | ||
1356 | if (options.startDate) publishedAtRange[ Op.gte ] = options.startDate | 1351 | if (options.startDate) publishedAtRange[Op.gte] = options.startDate |
1357 | if (options.endDate) publishedAtRange[ Op.lte ] = options.endDate | 1352 | if (options.endDate) publishedAtRange[Op.lte] = options.endDate |
1358 | 1353 | ||
1359 | whereAnd.push({ publishedAt: publishedAtRange }) | 1354 | whereAnd.push({ publishedAt: publishedAtRange }) |
1360 | } | 1355 | } |
@@ -1362,8 +1357,8 @@ export class VideoModel extends Model<VideoModel> { | |||
1362 | if (options.originallyPublishedStartDate || options.originallyPublishedEndDate) { | 1357 | if (options.originallyPublishedStartDate || options.originallyPublishedEndDate) { |
1363 | const originallyPublishedAtRange = {} | 1358 | const originallyPublishedAtRange = {} |
1364 | 1359 | ||
1365 | if (options.originallyPublishedStartDate) originallyPublishedAtRange[ Op.gte ] = options.originallyPublishedStartDate | 1360 | if (options.originallyPublishedStartDate) originallyPublishedAtRange[Op.gte] = options.originallyPublishedStartDate |
1366 | if (options.originallyPublishedEndDate) originallyPublishedAtRange[ Op.lte ] = options.originallyPublishedEndDate | 1361 | if (options.originallyPublishedEndDate) originallyPublishedAtRange[Op.lte] = options.originallyPublishedEndDate |
1367 | 1362 | ||
1368 | whereAnd.push({ originallyPublishedAt: originallyPublishedAtRange }) | 1363 | whereAnd.push({ originallyPublishedAt: originallyPublishedAtRange }) |
1369 | } | 1364 | } |
@@ -1371,8 +1366,8 @@ export class VideoModel extends Model<VideoModel> { | |||
1371 | if (options.durationMin || options.durationMax) { | 1366 | if (options.durationMin || options.durationMax) { |
1372 | const durationRange = {} | 1367 | const durationRange = {} |
1373 | 1368 | ||
1374 | if (options.durationMin) durationRange[ Op.gte ] = options.durationMin | 1369 | if (options.durationMin) durationRange[Op.gte] = options.durationMin |
1375 | if (options.durationMax) durationRange[ Op.lte ] = options.durationMax | 1370 | if (options.durationMax) durationRange[Op.lte] = options.durationMax |
1376 | 1371 | ||
1377 | whereAnd.push({ duration: durationRange }) | 1372 | whereAnd.push({ duration: durationRange }) |
1378 | } | 1373 | } |
@@ -1383,7 +1378,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1383 | if (options.search) { | 1378 | if (options.search) { |
1384 | const trigramSearch = { | 1379 | const trigramSearch = { |
1385 | id: { | 1380 | id: { |
1386 | [ Op.in ]: Sequelize.literal( | 1381 | [Op.in]: Sequelize.literal( |
1387 | '(' + | 1382 | '(' + |
1388 | 'SELECT "video"."id" FROM "video" ' + | 1383 | 'SELECT "video"."id" FROM "video" ' + |
1389 | 'WHERE ' + | 1384 | 'WHERE ' + |
@@ -1472,6 +1467,24 @@ export class VideoModel extends Model<VideoModel> { | |||
1472 | ]).findOne(options) | 1467 | ]).findOne(options) |
1473 | } | 1468 | } |
1474 | 1469 | ||
1470 | static loadImmutableAttributes (id: number | string, t?: Transaction): Bluebird<MVideoImmutable> { | ||
1471 | const fun = () => { | ||
1472 | const query = { | ||
1473 | where: buildWhereIdOrUUID(id), | ||
1474 | transaction: t | ||
1475 | } | ||
1476 | |||
1477 | return VideoModel.scope(ScopeNames.WITH_IMMUTABLE_ATTRIBUTES).findOne(query) | ||
1478 | } | ||
1479 | |||
1480 | return ModelCache.Instance.doCache({ | ||
1481 | cacheType: 'load-video-immutable-id', | ||
1482 | key: '' + id, | ||
1483 | deleteKey: 'video', | ||
1484 | fun | ||
1485 | }) | ||
1486 | } | ||
1487 | |||
1475 | static loadWithRights (id: number | string, t?: Transaction): Bluebird<MVideoWithRights> { | 1488 | static loadWithRights (id: number | string, t?: Transaction): Bluebird<MVideoWithRights> { |
1476 | const where = buildWhereIdOrUUID(id) | 1489 | const where = buildWhereIdOrUUID(id) |
1477 | const options = { | 1490 | const options = { |
@@ -1535,6 +1548,26 @@ export class VideoModel extends Model<VideoModel> { | |||
1535 | return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query) | 1548 | return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query) |
1536 | } | 1549 | } |
1537 | 1550 | ||
1551 | static loadByUrlImmutableAttributes (url: string, transaction?: Transaction): Bluebird<MVideoImmutable> { | ||
1552 | const fun = () => { | ||
1553 | const query: FindOptions = { | ||
1554 | where: { | ||
1555 | url | ||
1556 | }, | ||
1557 | transaction | ||
1558 | } | ||
1559 | |||
1560 | return VideoModel.scope(ScopeNames.WITH_IMMUTABLE_ATTRIBUTES).findOne(query) | ||
1561 | } | ||
1562 | |||
1563 | return ModelCache.Instance.doCache({ | ||
1564 | cacheType: 'load-video-immutable-url', | ||
1565 | key: url, | ||
1566 | deleteKey: 'video', | ||
1567 | fun | ||
1568 | }) | ||
1569 | } | ||
1570 | |||
1538 | static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Bluebird<MVideoAccountLightBlacklistAllFiles> { | 1571 | static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Bluebird<MVideoAccountLightBlacklistAllFiles> { |
1539 | const query: FindOptions = { | 1572 | const query: FindOptions = { |
1540 | where: { | 1573 | where: { |
@@ -1581,8 +1614,8 @@ export class VideoModel extends Model<VideoModel> { | |||
1581 | } | 1614 | } |
1582 | 1615 | ||
1583 | static loadForGetAPI (parameters: { | 1616 | static loadForGetAPI (parameters: { |
1584 | id: number | string, | 1617 | id: number | string |
1585 | t?: Transaction, | 1618 | t?: Transaction |
1586 | userId?: number | 1619 | userId?: number |
1587 | }): Bluebird<MVideoDetails> { | 1620 | }): Bluebird<MVideoDetails> { |
1588 | const { id, t, userId } = parameters | 1621 | const { id, t, userId } = parameters |
@@ -1648,9 +1681,9 @@ export class VideoModel extends Model<VideoModel> { | |||
1648 | static checkVideoHasInstanceFollow (videoId: number, followerActorId: number) { | 1681 | static checkVideoHasInstanceFollow (videoId: number, followerActorId: number) { |
1649 | // Instances only share videos | 1682 | // Instances only share videos |
1650 | const query = 'SELECT 1 FROM "videoShare" ' + | 1683 | const query = 'SELECT 1 FROM "videoShare" ' + |
1651 | 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' + | 1684 | 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' + |
1652 | 'WHERE "actorFollow"."actorId" = $followerActorId AND "videoShare"."videoId" = $videoId ' + | 1685 | 'WHERE "actorFollow"."actorId" = $followerActorId AND "videoShare"."videoId" = $videoId ' + |
1653 | 'LIMIT 1' | 1686 | 'LIMIT 1' |
1654 | 1687 | ||
1655 | const options = { | 1688 | const options = { |
1656 | type: QueryTypes.SELECT as QueryTypes.SELECT, | 1689 | type: QueryTypes.SELECT as QueryTypes.SELECT, |
@@ -1682,7 +1715,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1682 | } | 1715 | } |
1683 | 1716 | ||
1684 | return VideoModel.findAll(query) | 1717 | return VideoModel.findAll(query) |
1685 | .then(videos => videos.map(v => v.id)) | 1718 | .then(videos => videos.map(v => v.id)) |
1686 | } | 1719 | } |
1687 | 1720 | ||
1688 | // threshold corresponds to how many video the field should have to be returned | 1721 | // threshold corresponds to how many video the field should have to be returned |
@@ -1702,14 +1735,14 @@ export class VideoModel extends Model<VideoModel> { | |||
1702 | limit: count, | 1735 | limit: count, |
1703 | group: field, | 1736 | group: field, |
1704 | having: Sequelize.where( | 1737 | having: Sequelize.where( |
1705 | Sequelize.fn('COUNT', Sequelize.col(field)), { [ Op.gte ]: threshold } | 1738 | Sequelize.fn('COUNT', Sequelize.col(field)), { [Op.gte]: threshold } |
1706 | ), | 1739 | ), |
1707 | order: [ (this.sequelize as any).random() ] | 1740 | order: [ (this.sequelize as any).random() ] |
1708 | } | 1741 | } |
1709 | 1742 | ||
1710 | return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST_IDS, scopeOptions ] }) | 1743 | return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST_IDS, scopeOptions ] }) |
1711 | .findAll(query) | 1744 | .findAll(query) |
1712 | .then(rows => rows.map(r => r[ field ])) | 1745 | .then(rows => rows.map(r => r[field])) |
1713 | } | 1746 | } |
1714 | 1747 | ||
1715 | static buildTrendingQuery (trendingDays: number) { | 1748 | static buildTrendingQuery (trendingDays: number) { |
@@ -1720,7 +1753,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1720 | required: false, | 1753 | required: false, |
1721 | where: { | 1754 | where: { |
1722 | startDate: { | 1755 | startDate: { |
1723 | [ Op.gte ]: new Date(new Date().getTime() - (24 * 3600 * 1000) * trendingDays) | 1756 | [Op.gte]: new Date(new Date().getTime() - (24 * 3600 * 1000) * trendingDays) |
1724 | } | 1757 | } |
1725 | } | 1758 | } |
1726 | } | 1759 | } |
@@ -1803,23 +1836,23 @@ export class VideoModel extends Model<VideoModel> { | |||
1803 | } | 1836 | } |
1804 | 1837 | ||
1805 | static getCategoryLabel (id: number) { | 1838 | static getCategoryLabel (id: number) { |
1806 | return VIDEO_CATEGORIES[ id ] || 'Misc' | 1839 | return VIDEO_CATEGORIES[id] || 'Misc' |
1807 | } | 1840 | } |
1808 | 1841 | ||
1809 | static getLicenceLabel (id: number) { | 1842 | static getLicenceLabel (id: number) { |
1810 | return VIDEO_LICENCES[ id ] || 'Unknown' | 1843 | return VIDEO_LICENCES[id] || 'Unknown' |
1811 | } | 1844 | } |
1812 | 1845 | ||
1813 | static getLanguageLabel (id: string) { | 1846 | static getLanguageLabel (id: string) { |
1814 | return VIDEO_LANGUAGES[ id ] || 'Unknown' | 1847 | return VIDEO_LANGUAGES[id] || 'Unknown' |
1815 | } | 1848 | } |
1816 | 1849 | ||
1817 | static getPrivacyLabel (id: number) { | 1850 | static getPrivacyLabel (id: number) { |
1818 | return VIDEO_PRIVACIES[ id ] || 'Unknown' | 1851 | return VIDEO_PRIVACIES[id] || 'Unknown' |
1819 | } | 1852 | } |
1820 | 1853 | ||
1821 | static getStateLabel (id: number) { | 1854 | static getStateLabel (id: number) { |
1822 | return VIDEO_STATES[ id ] || 'Unknown' | 1855 | return VIDEO_STATES[id] || 'Unknown' |
1823 | } | 1856 | } |
1824 | 1857 | ||
1825 | isBlacklisted () { | 1858 | isBlacklisted () { |
@@ -1831,7 +1864,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1831 | this.VideoChannel.Account.isBlocked() | 1864 | this.VideoChannel.Account.isBlocked() |
1832 | } | 1865 | } |
1833 | 1866 | ||
1834 | getQualityFileBy <T extends MVideoWithFile> (this: T, fun: (files: MVideoFile[], it: (file: MVideoFile) => number) => MVideoFile) { | 1867 | getQualityFileBy<T extends MVideoWithFile> (this: T, fun: (files: MVideoFile[], it: (file: MVideoFile) => number) => MVideoFile) { |
1835 | if (Array.isArray(this.VideoFiles) && this.VideoFiles.length !== 0) { | 1868 | if (Array.isArray(this.VideoFiles) && this.VideoFiles.length !== 0) { |
1836 | const file = fun(this.VideoFiles, file => file.resolution) | 1869 | const file = fun(this.VideoFiles, file => file.resolution) |
1837 | 1870 | ||
@@ -1849,15 +1882,15 @@ export class VideoModel extends Model<VideoModel> { | |||
1849 | return undefined | 1882 | return undefined |
1850 | } | 1883 | } |
1851 | 1884 | ||
1852 | getMaxQualityFile <T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo { | 1885 | getMaxQualityFile<T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo { |
1853 | return this.getQualityFileBy(maxBy) | 1886 | return this.getQualityFileBy(maxBy) |
1854 | } | 1887 | } |
1855 | 1888 | ||
1856 | getMinQualityFile <T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo { | 1889 | getMinQualityFile<T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo { |
1857 | return this.getQualityFileBy(minBy) | 1890 | return this.getQualityFileBy(minBy) |
1858 | } | 1891 | } |
1859 | 1892 | ||
1860 | getWebTorrentFile <T extends MVideoWithFile> (this: T, resolution: number): MVideoFileVideo { | 1893 | getWebTorrentFile<T extends MVideoWithFile> (this: T, resolution: number): MVideoFileVideo { |
1861 | if (Array.isArray(this.VideoFiles) === false) return undefined | 1894 | if (Array.isArray(this.VideoFiles) === false) return undefined |
1862 | 1895 | ||
1863 | const file = this.VideoFiles.find(f => f.resolution === resolution) | 1896 | const file = this.VideoFiles.find(f => f.resolution === resolution) |
@@ -1893,6 +1926,10 @@ export class VideoModel extends Model<VideoModel> { | |||
1893 | return this.uuid + '.jpg' | 1926 | return this.uuid + '.jpg' |
1894 | } | 1927 | } |
1895 | 1928 | ||
1929 | hasPreview () { | ||
1930 | return !!this.getPreview() | ||
1931 | } | ||
1932 | |||
1896 | getPreview () { | 1933 | getPreview () { |
1897 | if (Array.isArray(this.Thumbnails) === false) return undefined | 1934 | if (Array.isArray(this.Thumbnails) === false) return undefined |
1898 | 1935 | ||
@@ -1980,8 +2017,8 @@ export class VideoModel extends Model<VideoModel> { | |||
1980 | } | 2017 | } |
1981 | 2018 | ||
1982 | this.VideoStreamingPlaylists = this.VideoStreamingPlaylists | 2019 | this.VideoStreamingPlaylists = this.VideoStreamingPlaylists |
1983 | .filter(s => s.type !== VideoStreamingPlaylistType.HLS) | 2020 | .filter(s => s.type !== VideoStreamingPlaylistType.HLS) |
1984 | .concat(toAdd) | 2021 | .concat(toAdd) |
1985 | } | 2022 | } |
1986 | 2023 | ||
1987 | removeFile (videoFile: MVideoFile, isRedundancy = false) { | 2024 | removeFile (videoFile: MVideoFile, isRedundancy = false) { |
@@ -2002,7 +2039,7 @@ export class VideoModel extends Model<VideoModel> { | |||
2002 | await remove(directoryPath) | 2039 | await remove(directoryPath) |
2003 | 2040 | ||
2004 | if (isRedundancy !== true) { | 2041 | if (isRedundancy !== true) { |
2005 | let streamingPlaylistWithFiles = streamingPlaylist as MStreamingPlaylistFilesVideo | 2042 | const streamingPlaylistWithFiles = streamingPlaylist as MStreamingPlaylistFilesVideo |
2006 | streamingPlaylistWithFiles.Video = this | 2043 | streamingPlaylistWithFiles.Video = this |
2007 | 2044 | ||
2008 | if (!Array.isArray(streamingPlaylistWithFiles.VideoFiles)) { | 2045 | if (!Array.isArray(streamingPlaylistWithFiles.VideoFiles)) { |