aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-06-19 14:55:58 +0200
committerChocobozzz <me@florianbigard.com>2019-06-19 15:05:36 +0200
commit3caf77d3b11f2dbc12e52d665183d36604c1dab9 (patch)
tree53e08727d5f1dc8be2bd4f4a14dadc05f607a9fb /server
parentbbe078ba55be635b5fc92f8f6286c45792b9e7e5 (diff)
downloadPeerTube-3caf77d3b11f2dbc12e52d665183d36604c1dab9.tar.gz
PeerTube-3caf77d3b11f2dbc12e52d665183d36604c1dab9.tar.zst
PeerTube-3caf77d3b11f2dbc12e52d665183d36604c1dab9.zip
Add language filters in user preferences
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/users/me.ts1
-rw-r--r--server/helpers/custom-validators/users.ts7
-rw-r--r--server/initializers/constants.ts3
-rw-r--r--server/initializers/migrations/0395-user-video-languages.ts25
-rw-r--r--server/middlewares/validators/users.ts5
-rw-r--r--server/models/account/user.ts8
-rw-r--r--server/models/utils.ts12
-rw-r--r--server/models/video/video.ts209
-rw-r--r--server/tests/api/check-params/users.ts23
-rw-r--r--server/tests/api/search/search-videos.ts53
10 files changed, 245 insertions, 101 deletions
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts
index 1750a02e9..a078334fe 100644
--- a/server/controllers/api/users/me.ts
+++ b/server/controllers/api/users/me.ts
@@ -182,6 +182,7 @@ async function updateMe (req: express.Request, res: express.Response) {
182 if (body.webTorrentEnabled !== undefined) user.webTorrentEnabled = body.webTorrentEnabled 182 if (body.webTorrentEnabled !== undefined) user.webTorrentEnabled = body.webTorrentEnabled
183 if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo 183 if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo
184 if (body.videosHistoryEnabled !== undefined) user.videosHistoryEnabled = body.videosHistoryEnabled 184 if (body.videosHistoryEnabled !== undefined) user.videosHistoryEnabled = body.videosHistoryEnabled
185 if (body.videoLanguages !== undefined) user.videoLanguages = body.videoLanguages
185 186
186 if (body.email !== undefined) { 187 if (body.email !== undefined) {
187 if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) { 188 if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts
index 56bc10b16..738d5cbbf 100644
--- a/server/helpers/custom-validators/users.ts
+++ b/server/helpers/custom-validators/users.ts
@@ -2,7 +2,7 @@ import 'express-validator'
2import * as validator from 'validator' 2import * as validator from 'validator'
3import { UserRole } from '../../../shared' 3import { UserRole } from '../../../shared'
4import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers/constants' 4import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers/constants'
5import { exists, isBooleanValid, isFileValid } from './misc' 5import { exists, isArray, isBooleanValid, isFileValid } from './misc'
6import { values } from 'lodash' 6import { values } from 'lodash'
7 7
8const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS 8const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS
@@ -54,6 +54,10 @@ function isUserAutoPlayVideoValid (value: any) {
54 return isBooleanValid(value) 54 return isBooleanValid(value)
55} 55}
56 56
57function isUserVideoLanguages (value: any) {
58 return value === null || (isArray(value) && value.length < CONSTRAINTS_FIELDS.USERS.VIDEO_LANGUAGES.max)
59}
60
57function isUserAdminFlagsValid (value: any) { 61function isUserAdminFlagsValid (value: any) {
58 return exists(value) && validator.isInt('' + value) 62 return exists(value) && validator.isInt('' + value)
59} 63}
@@ -84,6 +88,7 @@ export {
84 isUserVideosHistoryEnabledValid, 88 isUserVideosHistoryEnabledValid,
85 isUserBlockedValid, 89 isUserBlockedValid,
86 isUserPasswordValid, 90 isUserPasswordValid,
91 isUserVideoLanguages,
87 isUserBlockedReasonValid, 92 isUserBlockedReasonValid,
88 isUserRoleValid, 93 isUserRoleValid,
89 isUserVideoQuotaValid, 94 isUserVideoQuotaValid,
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index c2b8eff95..500f8770a 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -14,7 +14,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
14 14
15// --------------------------------------------------------------------------- 15// ---------------------------------------------------------------------------
16 16
17const LAST_MIGRATION_VERSION = 390 17const LAST_MIGRATION_VERSION = 395
18 18
19// --------------------------------------------------------------------------- 19// ---------------------------------------------------------------------------
20 20
@@ -177,6 +177,7 @@ let CONSTRAINTS_FIELDS = {
177 PASSWORD: { min: 6, max: 255 }, // Length 177 PASSWORD: { min: 6, max: 255 }, // Length
178 VIDEO_QUOTA: { min: -1 }, 178 VIDEO_QUOTA: { min: -1 },
179 VIDEO_QUOTA_DAILY: { min: -1 }, 179 VIDEO_QUOTA_DAILY: { min: -1 },
180 VIDEO_LANGUAGES: { max: 500 }, // Array length
180 BLOCKED_REASON: { min: 3, max: 250 } // Length 181 BLOCKED_REASON: { min: 3, max: 250 } // Length
181 }, 182 },
182 VIDEO_ABUSES: { 183 VIDEO_ABUSES: {
diff --git a/server/initializers/migrations/0395-user-video-languages.ts b/server/initializers/migrations/0395-user-video-languages.ts
new file mode 100644
index 000000000..278698bf4
--- /dev/null
+++ b/server/initializers/migrations/0395-user-video-languages.ts
@@ -0,0 +1,25 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction,
5 queryInterface: Sequelize.QueryInterface,
6 sequelize: Sequelize.Sequelize,
7 db: any
8}): Promise<void> {
9 const data = {
10 type: Sequelize.ARRAY(Sequelize.STRING),
11 allowNull: true,
12 defaultValue: null
13 }
14
15 await utils.queryInterface.addColumn('user', 'videoLanguages', data)
16}
17
18function down (options) {
19 throw new Error('Not implemented.')
20}
21
22export {
23 up,
24 down
25}
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts
index ec70fa0fd..947ed36c3 100644
--- a/server/middlewares/validators/users.ts
+++ b/server/middlewares/validators/users.ts
@@ -13,7 +13,7 @@ import {
13 isUserNSFWPolicyValid, 13 isUserNSFWPolicyValid,
14 isUserPasswordValid, 14 isUserPasswordValid,
15 isUserRoleValid, 15 isUserRoleValid,
16 isUserUsernameValid, 16 isUserUsernameValid, isUserVideoLanguages,
17 isUserVideoQuotaDailyValid, 17 isUserVideoQuotaDailyValid,
18 isUserVideoQuotaValid, 18 isUserVideoQuotaValid,
19 isUserVideosHistoryEnabledValid 19 isUserVideosHistoryEnabledValid
@@ -198,6 +198,9 @@ const usersUpdateMeValidator = [
198 body('autoPlayVideo') 198 body('autoPlayVideo')
199 .optional() 199 .optional()
200 .custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'), 200 .custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'),
201 body('videoLanguages')
202 .optional()
203 .custom(isUserVideoLanguages).withMessage('Should have a valid video languages attribute'),
201 body('videosHistoryEnabled') 204 body('videosHistoryEnabled')
202 .optional() 205 .optional()
203 .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled attribute'), 206 .custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled attribute'),
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index e75039521..aac691d66 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -31,6 +31,7 @@ import {
31 isUserPasswordValid, 31 isUserPasswordValid,
32 isUserRoleValid, 32 isUserRoleValid,
33 isUserUsernameValid, 33 isUserUsernameValid,
34 isUserVideoLanguages,
34 isUserVideoQuotaDailyValid, 35 isUserVideoQuotaDailyValid,
35 isUserVideoQuotaValid, 36 isUserVideoQuotaValid,
36 isUserVideosHistoryEnabledValid, 37 isUserVideosHistoryEnabledValid,
@@ -147,6 +148,12 @@ export class UserModel extends Model<UserModel> {
147 @Column 148 @Column
148 autoPlayVideo: boolean 149 autoPlayVideo: boolean
149 150
151 @AllowNull(true)
152 @Default(null)
153 @Is('UserVideoLanguages', value => throwIfNotValid(value, isUserVideoLanguages, 'video languages'))
154 @Column(DataType.ARRAY(DataType.STRING))
155 videoLanguages: string[]
156
150 @AllowNull(false) 157 @AllowNull(false)
151 @Default(UserAdminFlag.NONE) 158 @Default(UserAdminFlag.NONE)
152 @Is('UserAdminFlags', value => throwIfNotValid(value, isUserAdminFlagsValid, 'user admin flags')) 159 @Is('UserAdminFlags', value => throwIfNotValid(value, isUserAdminFlagsValid, 'user admin flags'))
@@ -551,6 +558,7 @@ export class UserModel extends Model<UserModel> {
551 webTorrentEnabled: this.webTorrentEnabled, 558 webTorrentEnabled: this.webTorrentEnabled,
552 videosHistoryEnabled: this.videosHistoryEnabled, 559 videosHistoryEnabled: this.videosHistoryEnabled,
553 autoPlayVideo: this.autoPlayVideo, 560 autoPlayVideo: this.autoPlayVideo,
561 videoLanguages: this.videoLanguages,
554 role: this.role, 562 role: this.role,
555 roleLabel: USER_ROLE_LABELS[ this.role ], 563 roleLabel: USER_ROLE_LABELS[ this.role ],
556 videoQuota: this.videoQuota, 564 videoQuota: this.videoQuota,
diff --git a/server/models/utils.ts b/server/models/utils.ts
index 2b172f608..206e108c3 100644
--- a/server/models/utils.ts
+++ b/server/models/utils.ts
@@ -1,7 +1,7 @@
1import { Sequelize } from 'sequelize-typescript' 1import { Model, Sequelize } from 'sequelize-typescript'
2import * as validator from 'validator' 2import * as validator from 'validator'
3import { OrderItem } from 'sequelize'
4import { Col } from 'sequelize/types/lib/utils' 3import { Col } from 'sequelize/types/lib/utils'
4import { OrderItem } from 'sequelize/types'
5 5
6type SortType = { sortModel: any, sortValue: string } 6type SortType = { sortModel: any, sortValue: string }
7 7
@@ -127,6 +127,11 @@ function parseAggregateResult (result: any) {
127 return total 127 return total
128} 128}
129 129
130const createSafeIn = (model: typeof Model, stringArr: string[]) => {
131 return stringArr.map(t => model.sequelize.escape(t))
132 .join(', ')
133}
134
130// --------------------------------------------------------------------------- 135// ---------------------------------------------------------------------------
131 136
132export { 137export {
@@ -141,7 +146,8 @@ export {
141 buildTrigramSearchIndex, 146 buildTrigramSearchIndex,
142 buildWhereIdOrUUID, 147 buildWhereIdOrUUID,
143 isOutdated, 148 isOutdated,
144 parseAggregateResult 149 parseAggregateResult,
150 createSafeIn
145} 151}
146 152
147// --------------------------------------------------------------------------- 153// ---------------------------------------------------------------------------
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index eccf0a4fa..92d07b5bc 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -83,6 +83,7 @@ import {
83 buildBlockedAccountSQL, 83 buildBlockedAccountSQL,
84 buildTrigramSearchIndex, 84 buildTrigramSearchIndex,
85 buildWhereIdOrUUID, 85 buildWhereIdOrUUID,
86 createSafeIn,
86 createSimilarityAttribute, 87 createSimilarityAttribute,
87 getVideoSort, 88 getVideoSort,
88 isOutdated, 89 isOutdated,
@@ -227,6 +228,8 @@ type AvailableForListIDsOptions = {
227 trendingDays?: number 228 trendingDays?: number
228 user?: UserModel, 229 user?: UserModel,
229 historyOfUser?: UserModel 230 historyOfUser?: UserModel
231
232 baseWhere?: WhereOptions[]
230} 233}
231 234
232@Scopes(() => ({ 235@Scopes(() => ({
@@ -270,34 +273,34 @@ type AvailableForListIDsOptions = {
270 return query 273 return query
271 }, 274 },
272 [ ScopeNames.AVAILABLE_FOR_LIST_IDS ]: (options: AvailableForListIDsOptions) => { 275 [ ScopeNames.AVAILABLE_FOR_LIST_IDS ]: (options: AvailableForListIDsOptions) => {
273 const attributes = options.withoutId === true ? [] : [ 'id' ] 276 const whereAnd = options.baseWhere ? options.baseWhere : []
274 277
275 const query: FindOptions = { 278 const query: FindOptions = {
276 raw: true, 279 raw: true,
277 attributes, 280 attributes: options.withoutId === true ? [] : [ 'id' ],
278 where: {
279 id: {
280 [ Op.and ]: [
281 {
282 [ Op.notIn ]: Sequelize.literal(
283 '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")'
284 )
285 }
286 ]
287 },
288 channelId: {
289 [ Op.notIn ]: Sequelize.literal(
290 '(' +
291 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' +
292 buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) +
293 ')' +
294 ')'
295 )
296 }
297 },
298 include: [] 281 include: []
299 } 282 }
300 283
284 whereAnd.push({
285 id: {
286 [ Op.notIn ]: Sequelize.literal(
287 '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")'
288 )
289 }
290 })
291
292 whereAnd.push({
293 channelId: {
294 [ Op.notIn ]: Sequelize.literal(
295 '(' +
296 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' +
297 buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) +
298 ')' +
299 ')'
300 )
301 }
302 })
303
301 // Only list public/published videos 304 // Only list public/published videos
302 if (!options.filter || options.filter !== 'all-local') { 305 if (!options.filter || options.filter !== 'all-local') {
303 const privacyWhere = { 306 const privacyWhere = {
@@ -317,7 +320,7 @@ type AvailableForListIDsOptions = {
317 ] 320 ]
318 } 321 }
319 322
320 Object.assign(query.where, privacyWhere) 323 whereAnd.push(privacyWhere)
321 } 324 }
322 325
323 if (options.videoPlaylistId) { 326 if (options.videoPlaylistId) {
@@ -387,86 +390,114 @@ type AvailableForListIDsOptions = {
387 390
388 // Force actorId to be a number to avoid SQL injections 391 // Force actorId to be a number to avoid SQL injections
389 const actorIdNumber = parseInt(options.followerActorId.toString(), 10) 392 const actorIdNumber = parseInt(options.followerActorId.toString(), 10)
390 query.where[ 'id' ][ Op.and ].push({ 393 whereAnd.push({
391 [ Op.in ]: Sequelize.literal( 394 id: {
392 '(' + 395 [ Op.in ]: Sequelize.literal(
393 'SELECT "videoShare"."videoId" AS "id" FROM "videoShare" ' + 396 '(' +
394 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' + 397 'SELECT "videoShare"."videoId" AS "id" FROM "videoShare" ' +
395 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + 398 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
396 ' UNION ALL ' + 399 'WHERE "actorFollow"."actorId" = ' + actorIdNumber +
397 'SELECT "video"."id" AS "id" FROM "video" ' + 400 ' UNION ALL ' +
398 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + 401 'SELECT "video"."id" AS "id" FROM "video" ' +
399 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' + 402 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
400 'INNER JOIN "actor" ON "account"."actorId" = "actor"."id" ' + 403 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' +
401 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "actor"."id" ' + 404 'INNER JOIN "actor" ON "account"."actorId" = "actor"."id" ' +
402 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + 405 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "actor"."id" ' +
403 localVideosReq + 406 'WHERE "actorFollow"."actorId" = ' + actorIdNumber +
404 ')' 407 localVideosReq +
405 ) 408 ')'
409 )
410 }
406 }) 411 })
407 } 412 }
408 413
409 if (options.withFiles === true) { 414 if (options.withFiles === true) {
410 query.where[ 'id' ][ Op.and ].push({ 415 whereAnd.push({
411 [ Op.in ]: Sequelize.literal( 416 id: {
412 '(SELECT "videoId" FROM "videoFile")' 417 [ Op.in ]: Sequelize.literal(
413 ) 418 '(SELECT "videoId" FROM "videoFile")'
419 )
420 }
414 }) 421 })
415 } 422 }
416 423
417 // FIXME: issues with sequelize count when making a join on n:m relation, so we just make a IN() 424 // FIXME: issues with sequelize count when making a join on n:m relation, so we just make a IN()
418 if (options.tagsAllOf || options.tagsOneOf) { 425 if (options.tagsAllOf || options.tagsOneOf) {
419 const createTagsIn = (tags: string[]) => {
420 return tags.map(t => VideoModel.sequelize.escape(t))
421 .join(', ')
422 }
423
424 if (options.tagsOneOf) { 426 if (options.tagsOneOf) {
425 query.where[ 'id' ][ Op.and ].push({ 427 whereAnd.push({
426 [ Op.in ]: Sequelize.literal( 428 id: {
427 '(' + 429 [ Op.in ]: Sequelize.literal(
428 'SELECT "videoId" FROM "videoTag" ' + 430 '(' +
429 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + 431 'SELECT "videoId" FROM "videoTag" ' +
430 'WHERE "tag"."name" IN (' + createTagsIn(options.tagsOneOf) + ')' + 432 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
431 ')' 433 'WHERE "tag"."name" IN (' + createSafeIn(VideoModel, options.tagsOneOf) + ')' +
432 ) 434 ')'
435 )
436 }
433 }) 437 })
434 } 438 }
435 439
436 if (options.tagsAllOf) { 440 if (options.tagsAllOf) {
437 query.where[ 'id' ][ Op.and ].push({ 441 whereAnd.push({
438 [ Op.in ]: Sequelize.literal( 442 id: {
439 '(' + 443 [ Op.in ]: Sequelize.literal(
440 'SELECT "videoId" FROM "videoTag" ' + 444 '(' +
441 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + 445 'SELECT "videoId" FROM "videoTag" ' +
442 'WHERE "tag"."name" IN (' + createTagsIn(options.tagsAllOf) + ')' + 446 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
443 'GROUP BY "videoTag"."videoId" HAVING COUNT(*) = ' + options.tagsAllOf.length + 447 'WHERE "tag"."name" IN (' + createSafeIn(VideoModel, options.tagsAllOf) + ')' +
444 ')' 448 'GROUP BY "videoTag"."videoId" HAVING COUNT(*) = ' + options.tagsAllOf.length +
445 ) 449 ')'
450 )
451 }
446 }) 452 })
447 } 453 }
448 } 454 }
449 455
450 if (options.nsfw === true || options.nsfw === false) { 456 if (options.nsfw === true || options.nsfw === false) {
451 query.where[ 'nsfw' ] = options.nsfw 457 whereAnd.push({ nsfw: options.nsfw })
452 } 458 }
453 459
454 if (options.categoryOneOf) { 460 if (options.categoryOneOf) {
455 query.where[ 'category' ] = { 461 whereAnd.push({
456 [ Op.or ]: options.categoryOneOf 462 category: {
457 } 463 [ Op.or ]: options.categoryOneOf
464 }
465 })
458 } 466 }
459 467
460 if (options.licenceOneOf) { 468 if (options.licenceOneOf) {
461 query.where[ 'licence' ] = { 469 whereAnd.push({
462 [ Op.or ]: options.licenceOneOf 470 licence: {
463 } 471 [ Op.or ]: options.licenceOneOf
472 }
473 })
464 } 474 }
465 475
466 if (options.languageOneOf) { 476 if (options.languageOneOf) {
467 query.where[ 'language' ] = { 477 let videoLanguages = options.languageOneOf
468 [ Op.or ]: options.languageOneOf 478 if (options.languageOneOf.find(l => l === '_unknown')) {
479 videoLanguages = videoLanguages.concat([ null ])
469 } 480 }
481
482 whereAnd.push({
483 [Op.or]: [
484 {
485 language: {
486 [ Op.or ]: videoLanguages
487 }
488 },
489 {
490 id: {
491 [ Op.in ]: Sequelize.literal(
492 '(' +
493 'SELECT "videoId" FROM "videoCaption" ' +
494 'WHERE "language" IN (' + createSafeIn(VideoModel, options.languageOneOf) + ') ' +
495 ')'
496 )
497 }
498 }
499 ]
500 })
470 } 501 }
471 502
472 if (options.trendingDays) { 503 if (options.trendingDays) {
@@ -490,6 +521,10 @@ type AvailableForListIDsOptions = {
490 query.subQuery = false 521 query.subQuery = false
491 } 522 }
492 523
524 query.where = {
525 [ Op.and ]: whereAnd
526 }
527
493 return query 528 return query
494 }, 529 },
495 [ ScopeNames.WITH_THUMBNAILS ]: { 530 [ ScopeNames.WITH_THUMBNAILS ]: {
@@ -1175,7 +1210,7 @@ export class VideoModel extends Model<VideoModel> {
1175 throw new Error('Try to filter all-local but no user has not the see all videos right') 1210 throw new Error('Try to filter all-local but no user has not the see all videos right')
1176 } 1211 }
1177 1212
1178 const query: FindOptions = { 1213 const query: FindOptions & { where?: null } = {
1179 offset: options.start, 1214 offset: options.start,
1180 limit: options.count, 1215 limit: options.count,
1181 order: getVideoSort(options.sort) 1216 order: getVideoSort(options.sort)
@@ -1299,16 +1334,13 @@ export class VideoModel extends Model<VideoModel> {
1299 ) 1334 )
1300 } 1335 }
1301 1336
1302 const query: FindOptions = { 1337 const query = {
1303 attributes: { 1338 attributes: {
1304 include: attributesInclude 1339 include: attributesInclude
1305 }, 1340 },
1306 offset: options.start, 1341 offset: options.start,
1307 limit: options.count, 1342 limit: options.count,
1308 order: getVideoSort(options.sort), 1343 order: getVideoSort(options.sort)
1309 where: {
1310 [ Op.and ]: whereAnd
1311 }
1312 } 1344 }
1313 1345
1314 const serverActor = await getServerActor() 1346 const serverActor = await getServerActor()
@@ -1323,7 +1355,8 @@ export class VideoModel extends Model<VideoModel> {
1323 tagsOneOf: options.tagsOneOf, 1355 tagsOneOf: options.tagsOneOf,
1324 tagsAllOf: options.tagsAllOf, 1356 tagsAllOf: options.tagsAllOf,
1325 user: options.user, 1357 user: options.user,
1326 filter: options.filter 1358 filter: options.filter,
1359 baseWhere: whereAnd
1327 } 1360 }
1328 1361
1329 return VideoModel.getAvailableForApi(query, queryOptions) 1362 return VideoModel.getAvailableForApi(query, queryOptions)
@@ -1590,7 +1623,7 @@ export class VideoModel extends Model<VideoModel> {
1590 } 1623 }
1591 1624
1592 private static async getAvailableForApi ( 1625 private static async getAvailableForApi (
1593 query: FindOptions, 1626 query: FindOptions & { where?: null }, // Forbid where field in query
1594 options: AvailableForListIDsOptions, 1627 options: AvailableForListIDsOptions,
1595 countVideos = true 1628 countVideos = true
1596 ) { 1629 ) {
@@ -1609,11 +1642,15 @@ export class VideoModel extends Model<VideoModel> {
1609 ] 1642 ]
1610 } 1643 }
1611 1644
1612 const [ count, rowsId ] = await Promise.all([ 1645 const [ count, ids ] = await Promise.all([
1613 countVideos ? VideoModel.scope(countScope).count(countQuery) : Promise.resolve<number>(undefined), 1646 countVideos
1614 VideoModel.scope(idsScope).findAll(query) 1647 ? VideoModel.scope(countScope).count(countQuery)
1648 : Promise.resolve<number>(undefined),
1649
1650 VideoModel.scope(idsScope)
1651 .findAll(query)
1652 .then(rows => rows.map(r => r.id))
1615 ]) 1653 ])
1616 const ids = rowsId.map(r => r.id)
1617 1654
1618 if (ids.length === 0) return { data: [], total: count } 1655 if (ids.length === 0) return { data: [], total: count }
1619 1656
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts
index 2316033a1..5d62fe2b3 100644
--- a/server/tests/api/check-params/users.ts
+++ b/server/tests/api/check-params/users.ts
@@ -364,6 +364,29 @@ describe('Test users API validators', function () {
364 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields }) 364 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
365 }) 365 })
366 366
367 it('Should fail with an invalid videoLanguages attribute', async function () {
368 {
369 const fields = {
370 videoLanguages: 'toto'
371 }
372
373 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
374 }
375
376 {
377 const languages = []
378 for (let i = 0; i < 1000; i++) {
379 languages.push('fr')
380 }
381
382 const fields = {
383 videoLanguages: languages
384 }
385
386 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
387 }
388 })
389
367 it('Should succeed to change password with the correct params', async function () { 390 it('Should succeed to change password with the correct params', async function () {
368 const fields = { 391 const fields = {
369 currentPassword: 'my super password', 392 currentPassword: 'my super password',
diff --git a/server/tests/api/search/search-videos.ts b/server/tests/api/search/search-videos.ts
index 92cc0dc71..c06200ffe 100644
--- a/server/tests/api/search/search-videos.ts
+++ b/server/tests/api/search/search-videos.ts
@@ -13,6 +13,7 @@ import {
13 uploadVideo, 13 uploadVideo,
14 wait 14 wait
15} from '../../../../shared/extra-utils' 15} from '../../../../shared/extra-utils'
16import { createVideoCaption } from '../../../../shared/extra-utils/videos/video-captions'
16 17
17const expect = chai.expect 18const expect = chai.expect
18 19
@@ -41,8 +42,29 @@ describe('Test videos search', function () {
41 const attributes2 = immutableAssign(attributes1, { name: attributes1.name + ' - 2', fixture: 'video_short.mp4' }) 42 const attributes2 = immutableAssign(attributes1, { name: attributes1.name + ' - 2', fixture: 'video_short.mp4' })
42 await uploadVideo(server.url, server.accessToken, attributes2) 43 await uploadVideo(server.url, server.accessToken, attributes2)
43 44
44 const attributes3 = immutableAssign(attributes1, { name: attributes1.name + ' - 3', language: 'en' }) 45 {
45 await uploadVideo(server.url, server.accessToken, attributes3) 46 const attributes3 = immutableAssign(attributes1, { name: attributes1.name + ' - 3', language: undefined })
47 const res = await uploadVideo(server.url, server.accessToken, attributes3)
48 const videoId = res.body.video.id
49
50 await createVideoCaption({
51 url: server.url,
52 accessToken: server.accessToken,
53 language: 'en',
54 videoId,
55 fixture: 'subtitle-good2.vtt',
56 mimeType: 'application/octet-stream'
57 })
58
59 await createVideoCaption({
60 url: server.url,
61 accessToken: server.accessToken,
62 language: 'aa',
63 videoId,
64 fixture: 'subtitle-good2.vtt',
65 mimeType: 'application/octet-stream'
66 })
67 }
46 68
47 const attributes4 = immutableAssign(attributes1, { name: attributes1.name + ' - 4', language: 'pl', nsfw: true }) 69 const attributes4 = immutableAssign(attributes1, { name: attributes1.name + ' - 4', language: 'pl', nsfw: true })
48 await uploadVideo(server.url, server.accessToken, attributes4) 70 await uploadVideo(server.url, server.accessToken, attributes4)
@@ -51,7 +73,7 @@ describe('Test videos search', function () {
51 73
52 startDate = new Date().toISOString() 74 startDate = new Date().toISOString()
53 75
54 const attributes5 = immutableAssign(attributes1, { name: attributes1.name + ' - 5', licence: 2 }) 76 const attributes5 = immutableAssign(attributes1, { name: attributes1.name + ' - 5', licence: 2, language: undefined })
55 await uploadVideo(server.url, server.accessToken, attributes5) 77 await uploadVideo(server.url, server.accessToken, attributes5)
56 78
57 const attributes6 = immutableAssign(attributes1, { name: attributes1.name + ' - 6', tags: [ 't1', 't2 '] }) 79 const attributes6 = immutableAssign(attributes1, { name: attributes1.name + ' - 6', tags: [ 't1', 't2 '] })
@@ -241,13 +263,26 @@ describe('Test videos search', function () {
241 search: '1111 2222 3333', 263 search: '1111 2222 3333',
242 languageOneOf: [ 'pl', 'en' ] 264 languageOneOf: [ 'pl', 'en' ]
243 } 265 }
244 const res1 = await advancedVideosSearch(server.url, query)
245 expect(res1.body.total).to.equal(2)
246 expect(res1.body.data[0].name).to.equal('1111 2222 3333 - 3')
247 expect(res1.body.data[1].name).to.equal('1111 2222 3333 - 4')
248 266
249 const res2 = await advancedVideosSearch(server.url, immutableAssign(query, { languageOneOf: [ 'eo' ] })) 267 {
250 expect(res2.body.total).to.equal(0) 268 const res = await advancedVideosSearch(server.url, query)
269 expect(res.body.total).to.equal(2)
270 expect(res.body.data[ 0 ].name).to.equal('1111 2222 3333 - 3')
271 expect(res.body.data[ 1 ].name).to.equal('1111 2222 3333 - 4')
272 }
273
274 {
275 const res = await advancedVideosSearch(server.url, immutableAssign(query, { languageOneOf: [ 'pl', 'en', '_unknown' ] }))
276 expect(res.body.total).to.equal(3)
277 expect(res.body.data[ 0 ].name).to.equal('1111 2222 3333 - 3')
278 expect(res.body.data[ 1 ].name).to.equal('1111 2222 3333 - 4')
279 expect(res.body.data[ 2 ].name).to.equal('1111 2222 3333 - 5')
280 }
281
282 {
283 const res = await advancedVideosSearch(server.url, immutableAssign(query, { languageOneOf: [ 'eo' ] }))
284 expect(res.body.total).to.equal(0)
285 }
251 }) 286 })
252 287
253 it('Should search by start date', async function () { 288 it('Should search by start date', async function () {