aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models
diff options
context:
space:
mode:
Diffstat (limited to 'server/models')
-rw-r--r--server/models/account/account-blocklist.ts2
-rw-r--r--server/models/account/account-video-rate.ts12
-rw-r--r--server/models/account/account.ts69
-rw-r--r--server/models/account/user-notification.ts4
-rw-r--r--server/models/account/user.ts90
-rw-r--r--server/models/activitypub/actor-follow.ts53
-rw-r--r--server/models/activitypub/actor.ts101
-rw-r--r--server/models/model-cache.ts91
-rw-r--r--server/models/oauth/oauth-token.ts4
-rw-r--r--server/models/redundancy/video-redundancy.ts218
-rw-r--r--server/models/server/plugin.ts8
-rw-r--r--server/models/server/server-blocklist.ts2
-rw-r--r--server/models/utils.ts4
-rw-r--r--server/models/video/thumbnail.ts15
-rw-r--r--server/models/video/video-abuse.ts6
-rw-r--r--server/models/video/video-caption.ts27
-rw-r--r--server/models/video/video-channel.ts33
-rw-r--r--server/models/video/video-comment.ts20
-rw-r--r--server/models/video/video-format-utils.ts34
-rw-r--r--server/models/video/video-playlist-element.ts8
-rw-r--r--server/models/video/video-playlist.ts36
-rw-r--r--server/models/video/video.ts372
22 files changed, 787 insertions, 422 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
32import { AccountBlocklistModel } from './account-blocklist' 32import { AccountBlocklistModel } from './account-blocklist'
33import { ServerBlocklistModel } from '../server/server-blocklist' 33import { ServerBlocklistModel } from '../server/server-blocklist'
34import { ActorFollowModel } from '../activitypub/actor-follow' 34import { ActorFollowModel } from '../activitypub/actor-follow'
35import { MAccountActor, MAccountDefault, MAccountSummaryFormattable, MAccountFormattable, MAccountAP } from '../../typings/models' 35import { MAccountActor, MAccountAP, MAccountDefault, MAccountFormattable, MAccountSummaryFormattable } from '../../typings/models'
36import * as Bluebird from 'bluebird' 36import * as Bluebird from 'bluebird'
37import { ModelCache } from '@server/models/model-cache'
37 38
38export enum ScopeNames { 39export 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 @@
1import { FindOptions, literal, Op, QueryTypes, where, fn, col } from 'sequelize' 1import { FindOptions, literal, Op, QueryTypes, where, fn, col, WhereOptions } from 'sequelize'
2import { 2import {
3 AfterDestroy, 3 AfterDestroy,
4 AfterUpdate, 4 AfterUpdate,
@@ -49,7 +49,7 @@ import { VideoPlaylistModel } from '../video/video-playlist'
49import { AccountModel } from './account' 49import { AccountModel } from './account'
50import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' 50import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
51import { values } from 'lodash' 51import { values } from 'lodash'
52import { DEFAULT_THEME_NAME, DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants' 52import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants'
53import { clearCacheByUserId } from '../../lib/oauth-model' 53import { clearCacheByUserId } from '../../lib/oauth-model'
54import { UserNotificationSettingModel } from './user-notification-setting' 54import { UserNotificationSettingModel } from './user-notification-setting'
55import { VideoModel } from '../video/video' 55import { 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 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { values, difference } from 'lodash' 2import { difference, values } from 'lodash'
3import { 3import {
4 AfterCreate, 4 AfterCreate,
5 AfterDestroy, 5 AfterDestroy,
@@ -23,7 +23,7 @@ import { logger } from '../../helpers/logger'
23import { getServerActor } from '../../helpers/utils' 23import { getServerActor } from '../../helpers/utils'
24import { ACTOR_FOLLOW_SCORE, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants' 24import { ACTOR_FOLLOW_SCORE, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants'
25import { ServerModel } from '../server/server' 25import { ServerModel } from '../server/server'
26import { createSafeIn, getSort, getFollowsSort } from '../utils' 26import { createSafeIn, getFollowsSort, getSort } from '../utils'
27import { ActorModel, unusedActorAttributesForAPI } from './actor' 27import { ActorModel, unusedActorAttributesForAPI } from './actor'
28import { VideoChannelModel } from '../video/video-channel' 28import { VideoChannelModel } from '../video/video-channel'
29import { AccountModel } from '../account/account' 29import { AccountModel } from '../account/account'
@@ -36,7 +36,6 @@ import {
36 MActorFollowSubscriptions 36 MActorFollowSubscriptions
37} from '@server/typings/models' 37} from '@server/typings/models'
38import { ActivityPubActorType } from '@shared/models' 38import { ActivityPubActorType } from '@shared/models'
39import { 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'
19import { ActivityPubActorType } from '../../../shared/models/activitypub' 19import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub'
20import { Avatar } from '../../../shared/models/avatars/avatar.model' 20import { Avatar } from '../../../shared/models/avatars/avatar.model'
21import { activityPubContextify } from '../../helpers/activitypub' 21import { activityPubContextify } from '../../helpers/activitypub'
22import { 22import {
@@ -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'
49import * as Bluebird from 'bluebird' 49import * as Bluebird from 'bluebird'
50import { Op, Transaction, literal } from 'sequelize' 50import { Op, Transaction, literal } from 'sequelize'
51import { ModelCache } from '@server/models/model-cache'
51 52
52enum ScopeNames { 53enum 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 @@
1import { Model } from 'sequelize-typescript'
2import * as Bluebird from 'bluebird'
3import { logger } from '@server/helpers/logger'
4
5type ModelCacheType =
6 'local-account-name'
7 | 'local-actor-name'
8 | 'local-actor-url'
9 | 'load-video-immutable-id'
10 | 'load-video-immutable-url'
11
12type DeleteKey =
13 'video'
14
15class 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
89export {
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
24export type OAuthTokenInfo = { 24export 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'
15import { ActorModel } from '../activitypub/actor' 15import { ActorModel } from '../activitypub/actor'
16import { getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils' 16import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils'
17import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc' 17import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc'
18import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants' 18import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants'
19import { VideoFileModel } from '../video/video-file' 19import { VideoFileModel } from '../video/video-file'
20import { getServerActor } from '../../helpers/utils' 20import { getServerActor } from '../../helpers/utils'
21import { VideoModel } from '../video/video' 21import { VideoModel } from '../video/video'
22import { VideoRedundancyStrategy } from '../../../shared/models/redundancy' 22import { VideoRedundancyStrategy, VideoRedundancyStrategyWithManual } from '../../../shared/models/redundancy'
23import { logger } from '../../helpers/logger' 23import { logger } from '../../helpers/logger'
24import { CacheFileObject, VideoPrivacy } from '../../../shared' 24import { CacheFileObject, VideoPrivacy } from '../../../shared'
25import { VideoChannelModel } from '../video/video-channel' 25import { VideoChannelModel } from '../video/video-channel'
@@ -27,17 +27,23 @@ import { ServerModel } from '../server/server'
27import { sample } from 'lodash' 27import { sample } from 'lodash'
28import { isTestInstance } from '../../helpers/core-utils' 28import { isTestInstance } from '../../helpers/core-utils'
29import * as Bluebird from 'bluebird' 29import * as Bluebird from 'bluebird'
30import { col, FindOptions, fn, literal, Op, Transaction } from 'sequelize' 30import { col, FindOptions, fn, literal, Op, Transaction, WhereOptions } from 'sequelize'
31import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist' 31import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist'
32import { CONFIG } from '../../initializers/config' 32import { CONFIG } from '../../initializers/config'
33import { MVideoRedundancy, MVideoRedundancyAP, MVideoRedundancyVideo } from '@server/typings/models' 33import { MVideoForRedundancyAPI, MVideoRedundancy, MVideoRedundancyAP, MVideoRedundancyVideo } from '@server/typings/models'
34import { VideoRedundanciesTarget } from '@shared/models/redundancy/video-redundancies-filters.model'
35import {
36 FileRedundancyInformation,
37 StreamingPlaylistRedundancyInformation,
38 VideoRedundancy
39} from '@shared/models/redundancy/video-redundancy.model'
34 40
35export enum ScopeNames { 41export 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
67function getBlacklistSort (model: any, value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { 67function 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
145function buildWhereIdOrUUID (id: number | string) { 145function 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'
19import { VideoModel } from './video' 19import { VideoModel } from './video'
20import { VideoPlaylistModel } from './video-playlist' 20import { VideoPlaylistModel } from './video-playlist'
21import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' 21import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
22import { MVideoAccountLight } from '@server/typings/models'
23import { 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'
16import { VideoModel } from './video' 17import { VideoModel } from './video'
17import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions' 18import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions'
18import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model' 19import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model'
19import { LAZY_STATIC_PATHS, VIDEO_LANGUAGES } from '../../initializers/constants' 20import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, VIDEO_LANGUAGES, WEBSERVER } from '../../initializers/constants'
20import { join } from 'path' 21import { join } from 'path'
21import { logger } from '../../helpers/logger' 22import { logger } from '../../helpers/logger'
22import { remove } from 'fs-extra' 23import { remove } from 'fs-extra'
23import { CONFIG } from '../../initializers/config' 24import { CONFIG } from '../../initializers/config'
24import * as Bluebird from 'bluebird' 25import * as Bluebird from 'bluebird'
25import { MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/typings/models' 26import { MVideoAccountLight, MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/typings/models'
27import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
26 28
27export enum ScopeNames { 29export 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
30import { VideoModel } from './video' 30import { VideoModel } from './video'
31import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' 31import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
32import { ServerModel } from '../server/server' 32import { ServerModel } from '../server/server'
33import { FindOptions, ModelIndexesOptions, Op } from 'sequelize' 33import { FindOptions, Op } from 'sequelize'
34import { AvatarModel } from '../avatar/avatar' 34import { AvatarModel } from '../avatar/avatar'
35import { VideoPlaylistModel } from './video-playlist' 35import { VideoPlaylistModel } from './video-playlist'
36import * as Bluebird from 'bluebird' 36import * 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
47const indexes: ModelIndexesOptions[] = [
48 buildTrigramSearchIndex('video_channel_name_trigram', 'name'),
49
50 {
51 fields: [ 'accountId' ]
52 },
53 {
54 fields: [ 'actorId' ]
55 }
56]
57
58export enum ScopeNames { 46export 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})
181export class VideoChannelModel extends Model<VideoChannelModel> { 178export 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'
27export type VideoFormattingJSONOptions = { 27export 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
36function videoModelToFormattedJSON (video: MVideoFormattable, options?: VideoFormattingJSONOptions): Video { 37function 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 eacffe186..bd4ca63ea 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -1,18 +1,7 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { maxBy, minBy } from 'lodash' 2import { maxBy, minBy } from 'lodash'
3import { join } from 'path' 3import { join } from 'path'
4import { 4import { 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'
16import { 5import {
17 AllowNull, 6 AllowNull,
18 BeforeDestroy, 7 BeforeDestroy,
@@ -131,87 +120,19 @@ 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,
138 MVideoWithFile, 127 MVideoWithFile,
139 MVideoWithRights, 128 MVideoWithRights
140 MStreamingPlaylistFiles
141} from '../../typings/models' 129} from '../../typings/models'
142import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../typings/models/video/video-file' 130import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../typings/models/video/video-file'
143import { MThumbnail } from '../../typings/models/video/thumbnail' 131import { MThumbnail } from '../../typings/models/video/thumbnail'
144import { VideoFile } from '@shared/models/videos/video-file.model' 132import { VideoFile } from '@shared/models/videos/video-file.model'
145import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' 133import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
146import validator from 'validator' 134import validator from 'validator'
147 135import { ModelCache } from '@server/models/model-cache'
148// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
149const indexes: (ModelIndexesOptions & { where?: WhereOptions })[] = [
150 buildTrigramSearchIndex('video_name_trigram', 'name'),
151
152 { fields: [ 'createdAt' ] },
153 {
154 fields: [
155 { name: 'publishedAt', order: 'DESC' },
156 { name: 'id', order: 'ASC' }
157 ]
158 },
159 { fields: [ 'duration' ] },
160 { fields: [ 'views' ] },
161 { fields: [ 'channelId' ] },
162 {
163 fields: [ 'originallyPublishedAt' ],
164 where: {
165 originallyPublishedAt: {
166 [Op.ne]: null
167 }
168 }
169 },
170 {
171 fields: [ 'category' ], // We don't care videos with an unknown category
172 where: {
173 category: {
174 [Op.ne]: null
175 }
176 }
177 },
178 {
179 fields: [ 'licence' ], // We don't care videos with an unknown licence
180 where: {
181 licence: {
182 [Op.ne]: null
183 }
184 }
185 },
186 {
187 fields: [ 'language' ], // We don't care videos with an unknown language
188 where: {
189 language: {
190 [Op.ne]: null
191 }
192 }
193 },
194 {
195 fields: [ 'nsfw' ], // Most of the videos are not NSFW
196 where: {
197 nsfw: true
198 }
199 },
200 {
201 fields: [ 'remote' ], // Only index local videos
202 where: {
203 remote: false
204 }
205 },
206 {
207 fields: [ 'uuid' ],
208 unique: true
209 },
210 {
211 fields: [ 'url' ],
212 unique: true
213 }
214]
215 136
216export enum ScopeNames { 137export enum ScopeNames {
217 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', 138 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
@@ -224,6 +145,7 @@ export enum ScopeNames {
224 WITH_USER_HISTORY = 'WITH_USER_HISTORY', 145 WITH_USER_HISTORY = 'WITH_USER_HISTORY',
225 WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', 146 WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS',
226 WITH_USER_ID = 'WITH_USER_ID', 147 WITH_USER_ID = 'WITH_USER_ID',
148 WITH_IMMUTABLE_ATTRIBUTES = 'WITH_IMMUTABLE_ATTRIBUTES',
227 WITH_THUMBNAILS = 'WITH_THUMBNAILS' 149 WITH_THUMBNAILS = 'WITH_THUMBNAILS'
228} 150}
229 151
@@ -267,7 +189,10 @@ export type AvailableForListIDsOptions = {
267} 189}
268 190
269@Scopes(() => ({ 191@Scopes(() => ({
270 [ ScopeNames.FOR_API ]: (options: ForAPIOptions) => { 192 [ScopeNames.WITH_IMMUTABLE_ATTRIBUTES]: {
193 attributes: [ 'id', 'url', 'uuid', 'remote' ]
194 },
195 [ScopeNames.FOR_API]: (options: ForAPIOptions) => {
271 const query: FindOptions = { 196 const query: FindOptions = {
272 include: [ 197 include: [
273 { 198 {
@@ -292,7 +217,7 @@ export type AvailableForListIDsOptions = {
292 if (options.ids) { 217 if (options.ids) {
293 query.where = { 218 query.where = {
294 id: { 219 id: {
295 [ Op.in ]: options.ids // FIXME: sequelize ANY seems broken 220 [Op.in]: options.ids
296 } 221 }
297 } 222 }
298 } 223 }
@@ -316,7 +241,7 @@ export type AvailableForListIDsOptions = {
316 241
317 return query 242 return query
318 }, 243 },
319 [ ScopeNames.AVAILABLE_FOR_LIST_IDS ]: (options: AvailableForListIDsOptions) => { 244 [ScopeNames.AVAILABLE_FOR_LIST_IDS]: (options: AvailableForListIDsOptions) => {
320 const whereAnd = options.baseWhere ? [].concat(options.baseWhere) : [] 245 const whereAnd = options.baseWhere ? [].concat(options.baseWhere) : []
321 246
322 const query: FindOptions = { 247 const query: FindOptions = {
@@ -327,11 +252,11 @@ export type AvailableForListIDsOptions = {
327 const attributesType = options.attributesType || 'id' 252 const attributesType = options.attributesType || 'id'
328 253
329 if (attributesType === 'id') query.attributes = [ 'id' ] 254 if (attributesType === 'id') query.attributes = [ 'id' ]
330 else if (attributesType === 'none') query.attributes = [ ] 255 else if (attributesType === 'none') query.attributes = []
331 256
332 whereAnd.push({ 257 whereAnd.push({
333 id: { 258 id: {
334 [ Op.notIn ]: Sequelize.literal( 259 [Op.notIn]: Sequelize.literal(
335 '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")' 260 '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")'
336 ) 261 )
337 } 262 }
@@ -340,7 +265,7 @@ export type AvailableForListIDsOptions = {
340 if (options.serverAccountId) { 265 if (options.serverAccountId) {
341 whereAnd.push({ 266 whereAnd.push({
342 channelId: { 267 channelId: {
343 [ Op.notIn ]: Sequelize.literal( 268 [Op.notIn]: Sequelize.literal(
344 '(' + 269 '(' +
345 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' + 270 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' +
346 buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) + 271 buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) +
@@ -353,15 +278,14 @@ export type AvailableForListIDsOptions = {
353 278
354 // Only list public/published videos 279 // Only list public/published videos
355 if (!options.filter || options.filter !== 'all-local') { 280 if (!options.filter || options.filter !== 'all-local') {
356
357 const publishWhere = { 281 const publishWhere = {
358 // 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
359 [ Op.or ]: [ 283 [Op.or]: [
360 { 284 {
361 state: VideoState.PUBLISHED 285 state: VideoState.PUBLISHED
362 }, 286 },
363 { 287 {
364 [ Op.and ]: { 288 [Op.and]: {
365 state: VideoState.TO_TRANSCODE, 289 state: VideoState.TO_TRANSCODE,
366 waitTranscoding: false 290 waitTranscoding: false
367 } 291 }
@@ -448,7 +372,7 @@ export type AvailableForListIDsOptions = {
448 [Op.or]: [ 372 [Op.or]: [
449 { 373 {
450 id: { 374 id: {
451 [ Op.in ]: Sequelize.literal( 375 [Op.in]: Sequelize.literal(
452 '(' + 376 '(' +
453 'SELECT "videoShare"."videoId" AS "id" FROM "videoShare" ' + 377 'SELECT "videoShare"."videoId" AS "id" FROM "videoShare" ' +
454 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' + 378 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
@@ -459,7 +383,7 @@ export type AvailableForListIDsOptions = {
459 }, 383 },
460 { 384 {
461 id: { 385 id: {
462 [ Op.in ]: Sequelize.literal( 386 [Op.in]: Sequelize.literal(
463 '(' + 387 '(' +
464 'SELECT "video"."id" AS "id" FROM "video" ' + 388 'SELECT "video"."id" AS "id" FROM "video" ' +
465 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + 389 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
@@ -479,7 +403,7 @@ export type AvailableForListIDsOptions = {
479 if (options.withFiles === true) { 403 if (options.withFiles === true) {
480 whereAnd.push({ 404 whereAnd.push({
481 id: { 405 id: {
482 [ Op.in ]: Sequelize.literal( 406 [Op.in]: Sequelize.literal(
483 '(SELECT "videoId" FROM "videoFile")' 407 '(SELECT "videoId" FROM "videoFile")'
484 ) 408 )
485 } 409 }
@@ -493,7 +417,7 @@ export type AvailableForListIDsOptions = {
493 417
494 whereAnd.push({ 418 whereAnd.push({
495 id: { 419 id: {
496 [ Op.in ]: Sequelize.literal( 420 [Op.in]: Sequelize.literal(
497 '(' + 421 '(' +
498 'SELECT "videoId" FROM "videoTag" ' + 422 'SELECT "videoId" FROM "videoTag" ' +
499 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + 423 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
@@ -509,7 +433,7 @@ export type AvailableForListIDsOptions = {
509 433
510 whereAnd.push({ 434 whereAnd.push({
511 id: { 435 id: {
512 [ Op.in ]: Sequelize.literal( 436 [Op.in]: Sequelize.literal(
513 '(' + 437 '(' +
514 'SELECT "videoId" FROM "videoTag" ' + 438 'SELECT "videoId" FROM "videoTag" ' +
515 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + 439 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
@@ -529,7 +453,7 @@ export type AvailableForListIDsOptions = {
529 if (options.categoryOneOf) { 453 if (options.categoryOneOf) {
530 whereAnd.push({ 454 whereAnd.push({
531 category: { 455 category: {
532 [ Op.or ]: options.categoryOneOf 456 [Op.or]: options.categoryOneOf
533 } 457 }
534 }) 458 })
535 } 459 }
@@ -537,7 +461,7 @@ export type AvailableForListIDsOptions = {
537 if (options.licenceOneOf) { 461 if (options.licenceOneOf) {
538 whereAnd.push({ 462 whereAnd.push({
539 licence: { 463 licence: {
540 [ Op.or ]: options.licenceOneOf 464 [Op.or]: options.licenceOneOf
541 } 465 }
542 }) 466 })
543 } 467 }
@@ -552,12 +476,12 @@ export type AvailableForListIDsOptions = {
552 [Op.or]: [ 476 [Op.or]: [
553 { 477 {
554 language: { 478 language: {
555 [ Op.or ]: videoLanguages 479 [Op.or]: videoLanguages
556 } 480 }
557 }, 481 },
558 { 482 {
559 id: { 483 id: {
560 [ Op.in ]: Sequelize.literal( 484 [Op.in]: Sequelize.literal(
561 '(' + 485 '(' +
562 'SELECT "videoId" FROM "videoCaption" ' + 486 'SELECT "videoId" FROM "videoCaption" ' +
563 'WHERE "language" IN (' + createSafeIn(VideoModel, options.languageOneOf) + ') ' + 487 'WHERE "language" IN (' + createSafeIn(VideoModel, options.languageOneOf) + ') ' +
@@ -591,12 +515,12 @@ export type AvailableForListIDsOptions = {
591 } 515 }
592 516
593 query.where = { 517 query.where = {
594 [ Op.and ]: whereAnd 518 [Op.and]: whereAnd
595 } 519 }
596 520
597 return query 521 return query
598 }, 522 },
599 [ ScopeNames.WITH_THUMBNAILS ]: { 523 [ScopeNames.WITH_THUMBNAILS]: {
600 include: [ 524 include: [
601 { 525 {
602 model: ThumbnailModel, 526 model: ThumbnailModel,
@@ -604,7 +528,7 @@ export type AvailableForListIDsOptions = {
604 } 528 }
605 ] 529 ]
606 }, 530 },
607 [ ScopeNames.WITH_USER_ID ]: { 531 [ScopeNames.WITH_USER_ID]: {
608 include: [ 532 include: [
609 { 533 {
610 attributes: [ 'accountId' ], 534 attributes: [ 'accountId' ],
@@ -620,7 +544,7 @@ export type AvailableForListIDsOptions = {
620 } 544 }
621 ] 545 ]
622 }, 546 },
623 [ ScopeNames.WITH_ACCOUNT_DETAILS ]: { 547 [ScopeNames.WITH_ACCOUNT_DETAILS]: {
624 include: [ 548 include: [
625 { 549 {
626 model: VideoChannelModel.unscoped(), 550 model: VideoChannelModel.unscoped(),
@@ -672,10 +596,10 @@ export type AvailableForListIDsOptions = {
672 } 596 }
673 ] 597 ]
674 }, 598 },
675 [ ScopeNames.WITH_TAGS ]: { 599 [ScopeNames.WITH_TAGS]: {
676 include: [ TagModel ] 600 include: [ TagModel ]
677 }, 601 },
678 [ ScopeNames.WITH_BLACKLISTED ]: { 602 [ScopeNames.WITH_BLACKLISTED]: {
679 include: [ 603 include: [
680 { 604 {
681 attributes: [ 'id', 'reason', 'unfederated' ], 605 attributes: [ 'id', 'reason', 'unfederated' ],
@@ -684,7 +608,7 @@ export type AvailableForListIDsOptions = {
684 } 608 }
685 ] 609 ]
686 }, 610 },
687 [ ScopeNames.WITH_WEBTORRENT_FILES ]: (withRedundancies = false) => { 611 [ScopeNames.WITH_WEBTORRENT_FILES]: (withRedundancies = false) => {
688 let subInclude: any[] = [] 612 let subInclude: any[] = []
689 613
690 if (withRedundancies === true) { 614 if (withRedundancies === true) {
@@ -708,7 +632,7 @@ export type AvailableForListIDsOptions = {
708 ] 632 ]
709 } 633 }
710 }, 634 },
711 [ ScopeNames.WITH_STREAMING_PLAYLISTS ]: (withRedundancies = false) => { 635 [ScopeNames.WITH_STREAMING_PLAYLISTS]: (withRedundancies = false) => {
712 const subInclude: IncludeOptions[] = [ 636 const subInclude: IncludeOptions[] = [
713 { 637 {
714 model: VideoFileModel.unscoped(), 638 model: VideoFileModel.unscoped(),
@@ -735,7 +659,7 @@ export type AvailableForListIDsOptions = {
735 ] 659 ]
736 } 660 }
737 }, 661 },
738 [ ScopeNames.WITH_SCHEDULED_UPDATE ]: { 662 [ScopeNames.WITH_SCHEDULED_UPDATE]: {
739 include: [ 663 include: [
740 { 664 {
741 model: ScheduleVideoUpdateModel.unscoped(), 665 model: ScheduleVideoUpdateModel.unscoped(),
@@ -743,7 +667,7 @@ export type AvailableForListIDsOptions = {
743 } 667 }
744 ] 668 ]
745 }, 669 },
746 [ ScopeNames.WITH_USER_HISTORY ]: (userId: number) => { 670 [ScopeNames.WITH_USER_HISTORY]: (userId: number) => {
747 return { 671 return {
748 include: [ 672 include: [
749 { 673 {
@@ -760,7 +684,72 @@ export type AvailableForListIDsOptions = {
760})) 684}))
761@Table({ 685@Table({
762 tableName: 'video', 686 tableName: 'video',
763 indexes 687 indexes: [
688 buildTrigramSearchIndex('video_name_trigram', 'name'),
689
690 { fields: [ 'createdAt' ] },
691 {
692 fields: [
693 { name: 'publishedAt', order: 'DESC' },
694 { name: 'id', order: 'ASC' }
695 ]
696 },
697 { fields: [ 'duration' ] },
698 { fields: [ 'views' ] },
699 { fields: [ 'channelId' ] },
700 {
701 fields: [ 'originallyPublishedAt' ],
702 where: {
703 originallyPublishedAt: {
704 [Op.ne]: null
705 }
706 }
707 },
708 {
709 fields: [ 'category' ], // We don't care videos with an unknown category
710 where: {
711 category: {
712 [Op.ne]: null
713 }
714 }
715 },
716 {
717 fields: [ 'licence' ], // We don't care videos with an unknown licence
718 where: {
719 licence: {
720 [Op.ne]: null
721 }
722 }
723 },
724 {
725 fields: [ 'language' ], // We don't care videos with an unknown language
726 where: {
727 language: {
728 [Op.ne]: null
729 }
730 }
731 },
732 {
733 fields: [ 'nsfw' ], // Most of the videos are not NSFW
734 where: {
735 nsfw: true
736 }
737 },
738 {
739 fields: [ 'remote' ], // Only index local videos
740 where: {
741 remote: false
742 }
743 },
744 {
745 fields: [ 'uuid' ],
746 unique: true
747 },
748 {
749 fields: [ 'url' ],
750 unique: true
751 }
752 ]
764}) 753})
765export class VideoModel extends Model<VideoModel> { 754export class VideoModel extends Model<VideoModel> {
766 755
@@ -1031,7 +1020,7 @@ export class VideoModel extends Model<VideoModel> {
1031 }, 1020 },
1032 onDelete: 'cascade', 1021 onDelete: 'cascade',
1033 hooks: true, 1022 hooks: true,
1034 [ 'separate' as any ]: true 1023 ['separate' as any]: true
1035 }) 1024 })
1036 VideoCaptions: VideoCaptionModel[] 1025 VideoCaptions: VideoCaptionModel[]
1037 1026
@@ -1090,6 +1079,11 @@ export class VideoModel extends Model<VideoModel> {
1090 return undefined 1079 return undefined
1091 } 1080 }
1092 1081
1082 @BeforeDestroy
1083 static invalidateCache (instance: VideoModel) {
1084 ModelCache.Instance.invalidateCache('video', instance.id)
1085 }
1086
1093 static listLocal (): Bluebird<MVideoWithAllFiles[]> { 1087 static listLocal (): Bluebird<MVideoWithAllFiles[]> {
1094 const query = { 1088 const query = {
1095 where: { 1089 where: {
@@ -1127,16 +1121,16 @@ export class VideoModel extends Model<VideoModel> {
1127 order: getVideoSort('createdAt', [ 'Tags', 'name', 'ASC' ] as any), // FIXME: sequelize typings 1121 order: getVideoSort('createdAt', [ 'Tags', 'name', 'ASC' ] as any), // FIXME: sequelize typings
1128 where: { 1122 where: {
1129 id: { 1123 id: {
1130 [ Op.in ]: Sequelize.literal('(' + rawQuery + ')') 1124 [Op.in]: Sequelize.literal('(' + rawQuery + ')')
1131 }, 1125 },
1132 [ Op.or ]: [ 1126 [Op.or]: [
1133 { privacy: VideoPrivacy.PUBLIC }, 1127 { privacy: VideoPrivacy.PUBLIC },
1134 { privacy: VideoPrivacy.UNLISTED } 1128 { privacy: VideoPrivacy.UNLISTED }
1135 ] 1129 ]
1136 }, 1130 },
1137 include: [ 1131 include: [
1138 { 1132 {
1139 attributes: [ 'language' ], 1133 attributes: [ 'language', 'fileUrl' ],
1140 model: VideoCaptionModel.unscoped(), 1134 model: VideoCaptionModel.unscoped(),
1141 required: false 1135 required: false
1142 }, 1136 },
@@ -1146,10 +1140,10 @@ export class VideoModel extends Model<VideoModel> {
1146 required: false, 1140 required: false,
1147 // We only want videos shared by this actor 1141 // We only want videos shared by this actor
1148 where: { 1142 where: {
1149 [ Op.and ]: [ 1143 [Op.and]: [
1150 { 1144 {
1151 id: { 1145 id: {
1152 [ Op.not ]: null 1146 [Op.not]: null
1153 } 1147 }
1154 }, 1148 },
1155 { 1149 {
@@ -1199,8 +1193,8 @@ export class VideoModel extends Model<VideoModel> {
1199 // totals: totalVideos + totalVideoShares 1193 // totals: totalVideos + totalVideoShares
1200 let totalVideos = 0 1194 let totalVideos = 0
1201 let totalVideoShares = 0 1195 let totalVideoShares = 0
1202 if (totals[ 0 ]) totalVideos = parseInt(totals[ 0 ].total, 10) 1196 if (totals[0]) totalVideos = parseInt(totals[0].total, 10)
1203 if (totals[ 1 ]) totalVideoShares = parseInt(totals[ 1 ].total, 10) 1197 if (totals[1]) totalVideoShares = parseInt(totals[1].total, 10)
1204 1198
1205 const total = totalVideos + totalVideoShares 1199 const total = totalVideos + totalVideoShares
1206 return { 1200 return {
@@ -1243,7 +1237,7 @@ export class VideoModel extends Model<VideoModel> {
1243 baseQuery = Object.assign(baseQuery, { 1237 baseQuery = Object.assign(baseQuery, {
1244 where: { 1238 where: {
1245 name: { 1239 name: {
1246 [ Op.iLike ]: '%' + search + '%' 1240 [Op.iLike]: '%' + search + '%'
1247 } 1241 }
1248 } 1242 }
1249 }) 1243 })
@@ -1273,25 +1267,25 @@ export class VideoModel extends Model<VideoModel> {
1273 } 1267 }
1274 1268
1275 static async listForApi (options: { 1269 static async listForApi (options: {
1276 start: number, 1270 start: number
1277 count: number, 1271 count: number
1278 sort: string, 1272 sort: string
1279 nsfw: boolean, 1273 nsfw: boolean
1280 includeLocalVideos: boolean, 1274 includeLocalVideos: boolean
1281 withFiles: boolean, 1275 withFiles: boolean
1282 categoryOneOf?: number[], 1276 categoryOneOf?: number[]
1283 licenceOneOf?: number[], 1277 licenceOneOf?: number[]
1284 languageOneOf?: string[], 1278 languageOneOf?: string[]
1285 tagsOneOf?: string[], 1279 tagsOneOf?: string[]
1286 tagsAllOf?: string[], 1280 tagsAllOf?: string[]
1287 filter?: VideoFilter, 1281 filter?: VideoFilter
1288 accountId?: number, 1282 accountId?: number
1289 videoChannelId?: number, 1283 videoChannelId?: number
1290 followerActorId?: number 1284 followerActorId?: number
1291 videoPlaylistId?: number, 1285 videoPlaylistId?: number
1292 trendingDays?: number, 1286 trendingDays?: number
1293 user?: MUserAccountId, 1287 user?: MUserAccountId
1294 historyOfUser?: MUserId, 1288 historyOfUser?: MUserId
1295 countVideos?: boolean 1289 countVideos?: boolean
1296 }) { 1290 }) {
1297 if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { 1291 if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
@@ -1357,7 +1351,7 @@ export class VideoModel extends Model<VideoModel> {
1357 tagsAllOf?: string[] 1351 tagsAllOf?: string[]
1358 durationMin?: number // seconds 1352 durationMin?: number // seconds
1359 durationMax?: number // seconds 1353 durationMax?: number // seconds
1360 user?: MUserAccountId, 1354 user?: MUserAccountId
1361 filter?: VideoFilter 1355 filter?: VideoFilter
1362 }) { 1356 }) {
1363 const whereAnd = [] 1357 const whereAnd = []
@@ -1365,8 +1359,8 @@ export class VideoModel extends Model<VideoModel> {
1365 if (options.startDate || options.endDate) { 1359 if (options.startDate || options.endDate) {
1366 const publishedAtRange = {} 1360 const publishedAtRange = {}
1367 1361
1368 if (options.startDate) publishedAtRange[ Op.gte ] = options.startDate 1362 if (options.startDate) publishedAtRange[Op.gte] = options.startDate
1369 if (options.endDate) publishedAtRange[ Op.lte ] = options.endDate 1363 if (options.endDate) publishedAtRange[Op.lte] = options.endDate
1370 1364
1371 whereAnd.push({ publishedAt: publishedAtRange }) 1365 whereAnd.push({ publishedAt: publishedAtRange })
1372 } 1366 }
@@ -1374,8 +1368,8 @@ export class VideoModel extends Model<VideoModel> {
1374 if (options.originallyPublishedStartDate || options.originallyPublishedEndDate) { 1368 if (options.originallyPublishedStartDate || options.originallyPublishedEndDate) {
1375 const originallyPublishedAtRange = {} 1369 const originallyPublishedAtRange = {}
1376 1370
1377 if (options.originallyPublishedStartDate) originallyPublishedAtRange[ Op.gte ] = options.originallyPublishedStartDate 1371 if (options.originallyPublishedStartDate) originallyPublishedAtRange[Op.gte] = options.originallyPublishedStartDate
1378 if (options.originallyPublishedEndDate) originallyPublishedAtRange[ Op.lte ] = options.originallyPublishedEndDate 1372 if (options.originallyPublishedEndDate) originallyPublishedAtRange[Op.lte] = options.originallyPublishedEndDate
1379 1373
1380 whereAnd.push({ originallyPublishedAt: originallyPublishedAtRange }) 1374 whereAnd.push({ originallyPublishedAt: originallyPublishedAtRange })
1381 } 1375 }
@@ -1383,8 +1377,8 @@ export class VideoModel extends Model<VideoModel> {
1383 if (options.durationMin || options.durationMax) { 1377 if (options.durationMin || options.durationMax) {
1384 const durationRange = {} 1378 const durationRange = {}
1385 1379
1386 if (options.durationMin) durationRange[ Op.gte ] = options.durationMin 1380 if (options.durationMin) durationRange[Op.gte] = options.durationMin
1387 if (options.durationMax) durationRange[ Op.lte ] = options.durationMax 1381 if (options.durationMax) durationRange[Op.lte] = options.durationMax
1388 1382
1389 whereAnd.push({ duration: durationRange }) 1383 whereAnd.push({ duration: durationRange })
1390 } 1384 }
@@ -1395,7 +1389,7 @@ export class VideoModel extends Model<VideoModel> {
1395 if (options.search) { 1389 if (options.search) {
1396 const trigramSearch = { 1390 const trigramSearch = {
1397 id: { 1391 id: {
1398 [ Op.in ]: Sequelize.literal( 1392 [Op.in]: Sequelize.literal(
1399 '(' + 1393 '(' +
1400 'SELECT "video"."id" FROM "video" ' + 1394 'SELECT "video"."id" FROM "video" ' +
1401 'WHERE ' + 1395 'WHERE ' +
@@ -1484,6 +1478,24 @@ export class VideoModel extends Model<VideoModel> {
1484 ]).findOne(options) 1478 ]).findOne(options)
1485 } 1479 }
1486 1480
1481 static loadImmutableAttributes (id: number | string, t?: Transaction): Bluebird<MVideoImmutable> {
1482 const fun = () => {
1483 const query = {
1484 where: buildWhereIdOrUUID(id),
1485 transaction: t
1486 }
1487
1488 return VideoModel.scope(ScopeNames.WITH_IMMUTABLE_ATTRIBUTES).findOne(query)
1489 }
1490
1491 return ModelCache.Instance.doCache({
1492 cacheType: 'load-video-immutable-id',
1493 key: '' + id,
1494 deleteKey: 'video',
1495 fun
1496 })
1497 }
1498
1487 static loadWithRights (id: number | string, t?: Transaction): Bluebird<MVideoWithRights> { 1499 static loadWithRights (id: number | string, t?: Transaction): Bluebird<MVideoWithRights> {
1488 const where = buildWhereIdOrUUID(id) 1500 const where = buildWhereIdOrUUID(id)
1489 const options = { 1501 const options = {
@@ -1547,6 +1559,26 @@ export class VideoModel extends Model<VideoModel> {
1547 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query) 1559 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query)
1548 } 1560 }
1549 1561
1562 static loadByUrlImmutableAttributes (url: string, transaction?: Transaction): Bluebird<MVideoImmutable> {
1563 const fun = () => {
1564 const query: FindOptions = {
1565 where: {
1566 url
1567 },
1568 transaction
1569 }
1570
1571 return VideoModel.scope(ScopeNames.WITH_IMMUTABLE_ATTRIBUTES).findOne(query)
1572 }
1573
1574 return ModelCache.Instance.doCache({
1575 cacheType: 'load-video-immutable-url',
1576 key: url,
1577 deleteKey: 'video',
1578 fun
1579 })
1580 }
1581
1550 static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Bluebird<MVideoAccountLightBlacklistAllFiles> { 1582 static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Bluebird<MVideoAccountLightBlacklistAllFiles> {
1551 const query: FindOptions = { 1583 const query: FindOptions = {
1552 where: { 1584 where: {
@@ -1593,8 +1625,8 @@ export class VideoModel extends Model<VideoModel> {
1593 } 1625 }
1594 1626
1595 static loadForGetAPI (parameters: { 1627 static loadForGetAPI (parameters: {
1596 id: number | string, 1628 id: number | string
1597 t?: Transaction, 1629 t?: Transaction
1598 userId?: number 1630 userId?: number
1599 }): Bluebird<MVideoDetails> { 1631 }): Bluebird<MVideoDetails> {
1600 const { id, t, userId } = parameters 1632 const { id, t, userId } = parameters
@@ -1660,9 +1692,9 @@ export class VideoModel extends Model<VideoModel> {
1660 static checkVideoHasInstanceFollow (videoId: number, followerActorId: number) { 1692 static checkVideoHasInstanceFollow (videoId: number, followerActorId: number) {
1661 // Instances only share videos 1693 // Instances only share videos
1662 const query = 'SELECT 1 FROM "videoShare" ' + 1694 const query = 'SELECT 1 FROM "videoShare" ' +
1663 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' + 1695 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
1664 'WHERE "actorFollow"."actorId" = $followerActorId AND "videoShare"."videoId" = $videoId ' + 1696 'WHERE "actorFollow"."actorId" = $followerActorId AND "videoShare"."videoId" = $videoId ' +
1665 'LIMIT 1' 1697 'LIMIT 1'
1666 1698
1667 const options = { 1699 const options = {
1668 type: QueryTypes.SELECT as QueryTypes.SELECT, 1700 type: QueryTypes.SELECT as QueryTypes.SELECT,
@@ -1694,7 +1726,7 @@ export class VideoModel extends Model<VideoModel> {
1694 } 1726 }
1695 1727
1696 return VideoModel.findAll(query) 1728 return VideoModel.findAll(query)
1697 .then(videos => videos.map(v => v.id)) 1729 .then(videos => videos.map(v => v.id))
1698 } 1730 }
1699 1731
1700 // threshold corresponds to how many video the field should have to be returned 1732 // threshold corresponds to how many video the field should have to be returned
@@ -1714,14 +1746,14 @@ export class VideoModel extends Model<VideoModel> {
1714 limit: count, 1746 limit: count,
1715 group: field, 1747 group: field,
1716 having: Sequelize.where( 1748 having: Sequelize.where(
1717 Sequelize.fn('COUNT', Sequelize.col(field)), { [ Op.gte ]: threshold } 1749 Sequelize.fn('COUNT', Sequelize.col(field)), { [Op.gte]: threshold }
1718 ), 1750 ),
1719 order: [ (this.sequelize as any).random() ] 1751 order: [ (this.sequelize as any).random() ]
1720 } 1752 }
1721 1753
1722 return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST_IDS, scopeOptions ] }) 1754 return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST_IDS, scopeOptions ] })
1723 .findAll(query) 1755 .findAll(query)
1724 .then(rows => rows.map(r => r[ field ])) 1756 .then(rows => rows.map(r => r[field]))
1725 } 1757 }
1726 1758
1727 static buildTrendingQuery (trendingDays: number) { 1759 static buildTrendingQuery (trendingDays: number) {
@@ -1732,7 +1764,7 @@ export class VideoModel extends Model<VideoModel> {
1732 required: false, 1764 required: false,
1733 where: { 1765 where: {
1734 startDate: { 1766 startDate: {
1735 [ Op.gte ]: new Date(new Date().getTime() - (24 * 3600 * 1000) * trendingDays) 1767 [Op.gte]: new Date(new Date().getTime() - (24 * 3600 * 1000) * trendingDays)
1736 } 1768 }
1737 } 1769 }
1738 } 1770 }
@@ -1815,23 +1847,23 @@ export class VideoModel extends Model<VideoModel> {
1815 } 1847 }
1816 1848
1817 static getCategoryLabel (id: number) { 1849 static getCategoryLabel (id: number) {
1818 return VIDEO_CATEGORIES[ id ] || 'Misc' 1850 return VIDEO_CATEGORIES[id] || 'Misc'
1819 } 1851 }
1820 1852
1821 static getLicenceLabel (id: number) { 1853 static getLicenceLabel (id: number) {
1822 return VIDEO_LICENCES[ id ] || 'Unknown' 1854 return VIDEO_LICENCES[id] || 'Unknown'
1823 } 1855 }
1824 1856
1825 static getLanguageLabel (id: string) { 1857 static getLanguageLabel (id: string) {
1826 return VIDEO_LANGUAGES[ id ] || 'Unknown' 1858 return VIDEO_LANGUAGES[id] || 'Unknown'
1827 } 1859 }
1828 1860
1829 static getPrivacyLabel (id: number) { 1861 static getPrivacyLabel (id: number) {
1830 return VIDEO_PRIVACIES[ id ] || 'Unknown' 1862 return VIDEO_PRIVACIES[id] || 'Unknown'
1831 } 1863 }
1832 1864
1833 static getStateLabel (id: number) { 1865 static getStateLabel (id: number) {
1834 return VIDEO_STATES[ id ] || 'Unknown' 1866 return VIDEO_STATES[id] || 'Unknown'
1835 } 1867 }
1836 1868
1837 isBlacklisted () { 1869 isBlacklisted () {
@@ -1843,7 +1875,7 @@ export class VideoModel extends Model<VideoModel> {
1843 this.VideoChannel.Account.isBlocked() 1875 this.VideoChannel.Account.isBlocked()
1844 } 1876 }
1845 1877
1846 getQualityFileBy <T extends MVideoWithFile> (this: T, fun: (files: MVideoFile[], it: (file: MVideoFile) => number) => MVideoFile) { 1878 getQualityFileBy<T extends MVideoWithFile> (this: T, fun: (files: MVideoFile[], it: (file: MVideoFile) => number) => MVideoFile) {
1847 if (Array.isArray(this.VideoFiles) && this.VideoFiles.length !== 0) { 1879 if (Array.isArray(this.VideoFiles) && this.VideoFiles.length !== 0) {
1848 const file = fun(this.VideoFiles, file => file.resolution) 1880 const file = fun(this.VideoFiles, file => file.resolution)
1849 1881
@@ -1861,15 +1893,15 @@ export class VideoModel extends Model<VideoModel> {
1861 return undefined 1893 return undefined
1862 } 1894 }
1863 1895
1864 getMaxQualityFile <T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo { 1896 getMaxQualityFile<T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo {
1865 return this.getQualityFileBy(maxBy) 1897 return this.getQualityFileBy(maxBy)
1866 } 1898 }
1867 1899
1868 getMinQualityFile <T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo { 1900 getMinQualityFile<T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo {
1869 return this.getQualityFileBy(minBy) 1901 return this.getQualityFileBy(minBy)
1870 } 1902 }
1871 1903
1872 getWebTorrentFile <T extends MVideoWithFile> (this: T, resolution: number): MVideoFileVideo { 1904 getWebTorrentFile<T extends MVideoWithFile> (this: T, resolution: number): MVideoFileVideo {
1873 if (Array.isArray(this.VideoFiles) === false) return undefined 1905 if (Array.isArray(this.VideoFiles) === false) return undefined
1874 1906
1875 const file = this.VideoFiles.find(f => f.resolution === resolution) 1907 const file = this.VideoFiles.find(f => f.resolution === resolution)
@@ -1905,6 +1937,10 @@ export class VideoModel extends Model<VideoModel> {
1905 return this.uuid + '.jpg' 1937 return this.uuid + '.jpg'
1906 } 1938 }
1907 1939
1940 hasPreview () {
1941 return !!this.getPreview()
1942 }
1943
1908 getPreview () { 1944 getPreview () {
1909 if (Array.isArray(this.Thumbnails) === false) return undefined 1945 if (Array.isArray(this.Thumbnails) === false) return undefined
1910 1946
@@ -1992,8 +2028,8 @@ export class VideoModel extends Model<VideoModel> {
1992 } 2028 }
1993 2029
1994 this.VideoStreamingPlaylists = this.VideoStreamingPlaylists 2030 this.VideoStreamingPlaylists = this.VideoStreamingPlaylists
1995 .filter(s => s.type !== VideoStreamingPlaylistType.HLS) 2031 .filter(s => s.type !== VideoStreamingPlaylistType.HLS)
1996 .concat(toAdd) 2032 .concat(toAdd)
1997 } 2033 }
1998 2034
1999 removeFile (videoFile: MVideoFile, isRedundancy = false) { 2035 removeFile (videoFile: MVideoFile, isRedundancy = false) {
@@ -2014,7 +2050,7 @@ export class VideoModel extends Model<VideoModel> {
2014 await remove(directoryPath) 2050 await remove(directoryPath)
2015 2051
2016 if (isRedundancy !== true) { 2052 if (isRedundancy !== true) {
2017 let streamingPlaylistWithFiles = streamingPlaylist as MStreamingPlaylistFilesVideo 2053 const streamingPlaylistWithFiles = streamingPlaylist as MStreamingPlaylistFilesVideo
2018 streamingPlaylistWithFiles.Video = this 2054 streamingPlaylistWithFiles.Video = this
2019 2055
2020 if (!Array.isArray(streamingPlaylistWithFiles.VideoFiles)) { 2056 if (!Array.isArray(streamingPlaylistWithFiles.VideoFiles)) {