aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2018-07-20 14:35:18 +0200
committerChocobozzz <me@florianbigard.com>2018-07-24 14:04:05 +0200
commitd525fc399a14a8b16eaad6d4c0bc0a9c4093c3c9 (patch)
tree4305044c4a97bdf1275b241c63cb0e85151cfb6a
parent57c36b277e68b764dd34cb2e449f6e2ca3d1e9b6 (diff)
downloadPeerTube-d525fc399a14a8b16eaad6d4c0bc0a9c4093c3c9.tar.gz
PeerTube-d525fc399a14a8b16eaad6d4c0bc0a9c4093c3c9.tar.zst
PeerTube-d525fc399a14a8b16eaad6d4c0bc0a9c4093c3c9.zip
Add videos list filters
-rw-r--r--server/controllers/api/accounts.ts12
-rw-r--r--server/controllers/api/search.ts16
-rw-r--r--server/controllers/api/video-channel.ts12
-rw-r--r--server/controllers/api/videos/index.ts12
-rw-r--r--server/controllers/feeds.ts5
-rw-r--r--server/helpers/custom-validators/misc.ts7
-rw-r--r--server/helpers/custom-validators/search.ts19
-rw-r--r--server/helpers/express-utils.ts14
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/middlewares/sort.ts2
-rw-r--r--server/middlewares/validators/search.ts46
-rw-r--r--server/models/utils.ts2
-rw-r--r--server/models/video/video.ts140
-rw-r--r--server/tests/api/check-params/index.ts1
-rw-r--r--server/tests/api/check-params/search.ts122
-rw-r--r--server/tests/api/index-fast.ts1
-rw-r--r--server/tests/api/search/search-videos.ts299
-rw-r--r--server/tests/api/videos/single-server.ts153
-rw-r--r--server/tests/api/videos/video-nsfw.ts30
-rw-r--r--server/tests/utils/index.ts1
-rw-r--r--server/tests/utils/requests/check-api-params.ts13
-rw-r--r--server/tests/utils/search/videos.ts77
-rw-r--r--server/tests/utils/videos/videos.ts86
-rw-r--r--shared/models/index.ts1
-rw-r--r--shared/models/search/index.ts1
-rw-r--r--shared/models/search/videos-search-query.model.ts24
26 files changed, 831 insertions, 267 deletions
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts
index 8e937276c..0117fc8c6 100644
--- a/server/controllers/api/accounts.ts
+++ b/server/controllers/api/accounts.ts
@@ -1,7 +1,7 @@
1import * as express from 'express' 1import * as express from 'express'
2import { getFormattedObjects } from '../../helpers/utils' 2import { getFormattedObjects } from '../../helpers/utils'
3import { 3import {
4 asyncMiddleware, 4 asyncMiddleware, commonVideosFiltersValidator,
5 listVideoAccountChannelsValidator, 5 listVideoAccountChannelsValidator,
6 optionalAuthenticate, 6 optionalAuthenticate,
7 paginationValidator, 7 paginationValidator,
@@ -11,7 +11,7 @@ import {
11import { accountsNameWithHostGetValidator, accountsSortValidator, videosSortValidator } from '../../middlewares/validators' 11import { accountsNameWithHostGetValidator, accountsSortValidator, videosSortValidator } from '../../middlewares/validators'
12import { AccountModel } from '../../models/account/account' 12import { AccountModel } from '../../models/account/account'
13import { VideoModel } from '../../models/video/video' 13import { VideoModel } from '../../models/video/video'
14import { isNSFWHidden } from '../../helpers/express-utils' 14import { buildNSFWFilter } from '../../helpers/express-utils'
15import { VideoChannelModel } from '../../models/video/video-channel' 15import { VideoChannelModel } from '../../models/video/video-channel'
16 16
17const accountsRouter = express.Router() 17const accountsRouter = express.Router()
@@ -36,6 +36,7 @@ accountsRouter.get('/:accountName/videos',
36 setDefaultSort, 36 setDefaultSort,
37 setDefaultPagination, 37 setDefaultPagination,
38 optionalAuthenticate, 38 optionalAuthenticate,
39 commonVideosFiltersValidator,
39 asyncMiddleware(listAccountVideos) 40 asyncMiddleware(listAccountVideos)
40) 41)
41 42
@@ -77,7 +78,12 @@ async function listAccountVideos (req: express.Request, res: express.Response, n
77 start: req.query.start, 78 start: req.query.start,
78 count: req.query.count, 79 count: req.query.count,
79 sort: req.query.sort, 80 sort: req.query.sort,
80 hideNSFW: isNSFWHidden(res), 81 categoryOneOf: req.query.categoryOneOf,
82 licenceOneOf: req.query.licenceOneOf,
83 languageOneOf: req.query.languageOneOf,
84 tagsOneOf: req.query.tagsOneOf,
85 tagsAllOf: req.query.tagsAllOf,
86 nsfw: buildNSFWFilter(res, req.query.nsfw),
81 withFiles: false, 87 withFiles: false,
82 accountId: account.id 88 accountId: account.id
83 }) 89 })
diff --git a/server/controllers/api/search.ts b/server/controllers/api/search.ts
index 2ff340b59..f810c7452 100644
--- a/server/controllers/api/search.ts
+++ b/server/controllers/api/search.ts
@@ -1,9 +1,10 @@
1import * as express from 'express' 1import * as express from 'express'
2import { isNSFWHidden } from '../../helpers/express-utils' 2import { buildNSFWFilter } from '../../helpers/express-utils'
3import { getFormattedObjects } from '../../helpers/utils' 3import { getFormattedObjects } from '../../helpers/utils'
4import { VideoModel } from '../../models/video/video' 4import { VideoModel } from '../../models/video/video'
5import { 5import {
6 asyncMiddleware, 6 asyncMiddleware,
7 commonVideosFiltersValidator,
7 optionalAuthenticate, 8 optionalAuthenticate,
8 paginationValidator, 9 paginationValidator,
9 searchValidator, 10 searchValidator,
@@ -11,6 +12,7 @@ import {
11 setDefaultSearchSort, 12 setDefaultSearchSort,
12 videosSearchSortValidator 13 videosSearchSortValidator
13} from '../../middlewares' 14} from '../../middlewares'
15import { VideosSearchQuery } from '../../../shared/models/search'
14 16
15const searchRouter = express.Router() 17const searchRouter = express.Router()
16 18
@@ -20,6 +22,7 @@ searchRouter.get('/videos',
20 videosSearchSortValidator, 22 videosSearchSortValidator,
21 setDefaultSearchSort, 23 setDefaultSearchSort,
22 optionalAuthenticate, 24 optionalAuthenticate,
25 commonVideosFiltersValidator,
23 searchValidator, 26 searchValidator,
24 asyncMiddleware(searchVideos) 27 asyncMiddleware(searchVideos)
25) 28)
@@ -31,13 +34,10 @@ export { searchRouter }
31// --------------------------------------------------------------------------- 34// ---------------------------------------------------------------------------
32 35
33async function searchVideos (req: express.Request, res: express.Response) { 36async function searchVideos (req: express.Request, res: express.Response) {
34 const resultList = await VideoModel.searchAndPopulateAccountAndServer( 37 const query: VideosSearchQuery = req.query
35 req.query.search as string, 38
36 req.query.start as number, 39 const options = Object.assign(query, { nsfw: buildNSFWFilter(res, query.nsfw) })
37 req.query.count as number, 40 const resultList = await VideoModel.searchAndPopulateAccountAndServer(options)
38 req.query.sort as string,
39 isNSFWHidden(res)
40 )
41 41
42 return res.json(getFormattedObjects(resultList.data, resultList.total)) 42 return res.json(getFormattedObjects(resultList.data, resultList.total))
43} 43}
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts
index 1707732ee..0488ba8f5 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -3,7 +3,7 @@ import { getFormattedObjects, resetSequelizeInstance } from '../../helpers/utils
3import { 3import {
4 asyncMiddleware, 4 asyncMiddleware,
5 asyncRetryTransactionMiddleware, 5 asyncRetryTransactionMiddleware,
6 authenticate, 6 authenticate, commonVideosFiltersValidator,
7 optionalAuthenticate, 7 optionalAuthenticate,
8 paginationValidator, 8 paginationValidator,
9 setDefaultPagination, 9 setDefaultPagination,
@@ -19,7 +19,7 @@ import { videosSortValidator } from '../../middlewares/validators'
19import { sendUpdateActor } from '../../lib/activitypub/send' 19import { sendUpdateActor } from '../../lib/activitypub/send'
20import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared' 20import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared'
21import { createVideoChannel } from '../../lib/video-channel' 21import { createVideoChannel } from '../../lib/video-channel'
22import { createReqFiles, isNSFWHidden } from '../../helpers/express-utils' 22import { createReqFiles, buildNSFWFilter } from '../../helpers/express-utils'
23import { setAsyncActorKeys } from '../../lib/activitypub' 23import { setAsyncActorKeys } from '../../lib/activitypub'
24import { AccountModel } from '../../models/account/account' 24import { AccountModel } from '../../models/account/account'
25import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers' 25import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers'
@@ -79,6 +79,7 @@ videoChannelRouter.get('/:id/videos',
79 setDefaultSort, 79 setDefaultSort,
80 setDefaultPagination, 80 setDefaultPagination,
81 optionalAuthenticate, 81 optionalAuthenticate,
82 commonVideosFiltersValidator,
82 asyncMiddleware(listVideoChannelVideos) 83 asyncMiddleware(listVideoChannelVideos)
83) 84)
84 85
@@ -189,7 +190,12 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon
189 start: req.query.start, 190 start: req.query.start,
190 count: req.query.count, 191 count: req.query.count,
191 sort: req.query.sort, 192 sort: req.query.sort,
192 hideNSFW: isNSFWHidden(res), 193 categoryOneOf: req.query.categoryOneOf,
194 licenceOneOf: req.query.licenceOneOf,
195 languageOneOf: req.query.languageOneOf,
196 tagsOneOf: req.query.tagsOneOf,
197 tagsAllOf: req.query.tagsAllOf,
198 nsfw: buildNSFWFilter(res, req.query.nsfw),
193 withFiles: false, 199 withFiles: false,
194 videoChannelId: videoChannelInstance.id 200 videoChannelId: videoChannelInstance.id
195 }) 201 })
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 547522123..101183eab 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -31,6 +31,7 @@ import {
31 asyncMiddleware, 31 asyncMiddleware,
32 asyncRetryTransactionMiddleware, 32 asyncRetryTransactionMiddleware,
33 authenticate, 33 authenticate,
34 commonVideosFiltersValidator,
34 optionalAuthenticate, 35 optionalAuthenticate,
35 paginationValidator, 36 paginationValidator,
36 setDefaultPagination, 37 setDefaultPagination,
@@ -49,7 +50,7 @@ import { blacklistRouter } from './blacklist'
49import { videoCommentRouter } from './comment' 50import { videoCommentRouter } from './comment'
50import { rateVideoRouter } from './rate' 51import { rateVideoRouter } from './rate'
51import { VideoFilter } from '../../../../shared/models/videos/video-query.type' 52import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
52import { createReqFiles, isNSFWHidden } from '../../../helpers/express-utils' 53import { createReqFiles, buildNSFWFilter } from '../../../helpers/express-utils'
53import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update' 54import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
54import { videoCaptionsRouter } from './captions' 55import { videoCaptionsRouter } from './captions'
55 56
@@ -90,6 +91,7 @@ videosRouter.get('/',
90 setDefaultSort, 91 setDefaultSort,
91 setDefaultPagination, 92 setDefaultPagination,
92 optionalAuthenticate, 93 optionalAuthenticate,
94 commonVideosFiltersValidator,
93 asyncMiddleware(listVideos) 95 asyncMiddleware(listVideos)
94) 96)
95videosRouter.put('/:id', 97videosRouter.put('/:id',
@@ -401,8 +403,12 @@ async function listVideos (req: express.Request, res: express.Response, next: ex
401 start: req.query.start, 403 start: req.query.start,
402 count: req.query.count, 404 count: req.query.count,
403 sort: req.query.sort, 405 sort: req.query.sort,
404 category: req.query.category, 406 categoryOneOf: req.query.categoryOneOf,
405 hideNSFW: isNSFWHidden(res), 407 licenceOneOf: req.query.licenceOneOf,
408 languageOneOf: req.query.languageOneOf,
409 tagsOneOf: req.query.tagsOneOf,
410 tagsAllOf: req.query.tagsAllOf,
411 nsfw: buildNSFWFilter(res, req.query.nsfw),
406 filter: req.query.filter as VideoFilter, 412 filter: req.query.filter as VideoFilter,
407 withFiles: false 413 withFiles: false
408 }) 414 })
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts
index ff6b423d9..682f4abda 100644
--- a/server/controllers/feeds.ts
+++ b/server/controllers/feeds.ts
@@ -8,6 +8,7 @@ import { AccountModel } from '../models/account/account'
8import { cacheRoute } from '../middlewares/cache' 8import { cacheRoute } from '../middlewares/cache'
9import { VideoChannelModel } from '../models/video/video-channel' 9import { VideoChannelModel } from '../models/video/video-channel'
10import { VideoCommentModel } from '../models/video/video-comment' 10import { VideoCommentModel } from '../models/video/video-comment'
11import { buildNSFWFilter } from '../helpers/express-utils'
11 12
12const feedsRouter = express.Router() 13const feedsRouter = express.Router()
13 14
@@ -73,7 +74,7 @@ async function generateVideoFeed (req: express.Request, res: express.Response, n
73 74
74 const account: AccountModel = res.locals.account 75 const account: AccountModel = res.locals.account
75 const videoChannel: VideoChannelModel = res.locals.videoChannel 76 const videoChannel: VideoChannelModel = res.locals.videoChannel
76 const hideNSFW = CONFIG.INSTANCE.DEFAULT_NSFW_POLICY === 'do_not_list' 77 const nsfw = buildNSFWFilter(res, req.query.nsfw)
77 78
78 let name: string 79 let name: string
79 let description: string 80 let description: string
@@ -95,7 +96,7 @@ async function generateVideoFeed (req: express.Request, res: express.Response, n
95 start, 96 start,
96 count: FEEDS.COUNT, 97 count: FEEDS.COUNT,
97 sort: req.query.sort, 98 sort: req.query.sort,
98 hideNSFW, 99 nsfw,
99 filter: req.query.filter, 100 filter: req.query.filter,
100 withFiles: true, 101 withFiles: true,
101 accountId: account ? account.id : null, 102 accountId: account ? account.id : null,
diff --git a/server/helpers/custom-validators/misc.ts b/server/helpers/custom-validators/misc.ts
index 455aae367..151fc852b 100644
--- a/server/helpers/custom-validators/misc.ts
+++ b/server/helpers/custom-validators/misc.ts
@@ -41,6 +41,12 @@ function toValueOrNull (value: string) {
41 return value 41 return value
42} 42}
43 43
44function toArray (value: string) {
45 if (value && isArray(value) === false) return [ value ]
46
47 return value
48}
49
44function isFileValid ( 50function isFileValid (
45 files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], 51 files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[],
46 mimeTypeRegex: string, 52 mimeTypeRegex: string,
@@ -80,5 +86,6 @@ export {
80 toValueOrNull, 86 toValueOrNull,
81 isBooleanValid, 87 isBooleanValid,
82 toIntOrNull, 88 toIntOrNull,
89 toArray,
83 isFileValid 90 isFileValid
84} 91}
diff --git a/server/helpers/custom-validators/search.ts b/server/helpers/custom-validators/search.ts
new file mode 100644
index 000000000..2fde39160
--- /dev/null
+++ b/server/helpers/custom-validators/search.ts
@@ -0,0 +1,19 @@
1import * as validator from 'validator'
2import 'express-validator'
3
4import { isArray } from './misc'
5
6function isNumberArray (value: any) {
7 return isArray(value) && value.every(v => validator.isInt('' + v))
8}
9
10function isStringArray (value: any) {
11 return isArray(value) && value.every(v => typeof v === 'string')
12}
13
14// ---------------------------------------------------------------------------
15
16export {
17 isNumberArray,
18 isStringArray
19}
diff --git a/server/helpers/express-utils.ts b/server/helpers/express-utils.ts
index d023117a8..5bf1e1a5f 100644
--- a/server/helpers/express-utils.ts
+++ b/server/helpers/express-utils.ts
@@ -5,13 +5,19 @@ import { logger } from './logger'
5import { User } from '../../shared/models/users' 5import { User } from '../../shared/models/users'
6import { generateRandomString } from './utils' 6import { generateRandomString } from './utils'
7 7
8function isNSFWHidden (res: express.Response) { 8function buildNSFWFilter (res: express.Response, paramNSFW?: boolean) {
9 if (paramNSFW === true || paramNSFW === false) return paramNSFW
10
9 if (res.locals.oauth) { 11 if (res.locals.oauth) {
10 const user: User = res.locals.oauth.token.User 12 const user: User = res.locals.oauth.token.User
11 if (user) return user.nsfwPolicy === 'do_not_list' 13 // User does not want NSFW videos
14 if (user && user.nsfwPolicy === 'do_not_list') return false
12 } 15 }
13 16
14 return CONFIG.INSTANCE.DEFAULT_NSFW_POLICY === 'do_not_list' 17 if (CONFIG.INSTANCE.DEFAULT_NSFW_POLICY === 'do_not_list') return false
18
19 // Display all
20 return null
15} 21}
16 22
17function getHostWithPort (host: string) { 23function getHostWithPort (host: string) {
@@ -70,7 +76,7 @@ function createReqFiles (
70// --------------------------------------------------------------------------- 76// ---------------------------------------------------------------------------
71 77
72export { 78export {
73 isNSFWHidden, 79 buildNSFWFilter,
74 getHostWithPort, 80 getHostWithPort,
75 badRequest, 81 badRequest,
76 createReqFiles 82 createReqFiles
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index b966c0acb..9f220aea5 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -37,7 +37,7 @@ const SORTABLE_COLUMNS = {
37 FOLLOWERS: [ 'createdAt' ], 37 FOLLOWERS: [ 'createdAt' ],
38 FOLLOWING: [ 'createdAt' ], 38 FOLLOWING: [ 'createdAt' ],
39 39
40 VIDEOS_SEARCH: [ 'bestmatch', 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes' ] 40 VIDEOS_SEARCH: [ 'match', 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes' ]
41} 41}
42 42
43const OAUTH_LIFETIME = { 43const OAUTH_LIFETIME = {
diff --git a/server/middlewares/sort.ts b/server/middlewares/sort.ts
index 6307ee154..8a62c8be6 100644
--- a/server/middlewares/sort.ts
+++ b/server/middlewares/sort.ts
@@ -9,7 +9,7 @@ function setDefaultSort (req: express.Request, res: express.Response, next: expr
9} 9}
10 10
11function setDefaultSearchSort (req: express.Request, res: express.Response, next: express.NextFunction) { 11function setDefaultSearchSort (req: express.Request, res: express.Response, next: express.NextFunction) {
12 if (!req.query.sort) req.query.sort = '-bestmatch' 12 if (!req.query.sort) req.query.sort = '-match'
13 13
14 return next() 14 return next()
15} 15}
diff --git a/server/middlewares/validators/search.ts b/server/middlewares/validators/search.ts
index 774845e8a..fb2148eb3 100644
--- a/server/middlewares/validators/search.ts
+++ b/server/middlewares/validators/search.ts
@@ -2,12 +2,55 @@ import * as express from 'express'
2import { areValidationErrors } from './utils' 2import { areValidationErrors } from './utils'
3import { logger } from '../../helpers/logger' 3import { logger } from '../../helpers/logger'
4import { query } from 'express-validator/check' 4import { query } from 'express-validator/check'
5import { isNumberArray, isStringArray } from '../../helpers/custom-validators/search'
6import { isBooleanValid, isDateValid, toArray } from '../../helpers/custom-validators/misc'
5 7
6const searchValidator = [ 8const searchValidator = [
7 query('search').not().isEmpty().withMessage('Should have a valid search'), 9 query('search').not().isEmpty().withMessage('Should have a valid search'),
8 10
11 query('startDate').optional().custom(isDateValid).withMessage('Should have a valid start date'),
12 query('endDate').optional().custom(isDateValid).withMessage('Should have a valid end date'),
13
14 query('durationMin').optional().isInt().withMessage('Should have a valid min duration'),
15 query('durationMax').optional().isInt().withMessage('Should have a valid max duration'),
16
17 (req: express.Request, res: express.Response, next: express.NextFunction) => {
18 logger.debug('Checking search query', { parameters: req.query })
19
20 if (areValidationErrors(req, res)) return
21
22 return next()
23 }
24]
25
26const commonVideosFiltersValidator = [
27 query('categoryOneOf')
28 .optional()
29 .customSanitizer(toArray)
30 .custom(isNumberArray).withMessage('Should have a valid one of category array'),
31 query('licenceOneOf')
32 .optional()
33 .customSanitizer(toArray)
34 .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
35 query('languageOneOf')
36 .optional()
37 .customSanitizer(toArray)
38 .custom(isStringArray).withMessage('Should have a valid one of language array'),
39 query('tagsOneOf')
40 .optional()
41 .customSanitizer(toArray)
42 .custom(isStringArray).withMessage('Should have a valid one of tags array'),
43 query('tagsAllOf')
44 .optional()
45 .customSanitizer(toArray)
46 .custom(isStringArray).withMessage('Should have a valid all of tags array'),
47 query('nsfw')
48 .optional()
49 .toBoolean()
50 .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
51
9 (req: express.Request, res: express.Response, next: express.NextFunction) => { 52 (req: express.Request, res: express.Response, next: express.NextFunction) => {
10 logger.debug('Checking search parameters', { parameters: req.params }) 53 logger.debug('Checking commons video filters query', { parameters: req.query })
11 54
12 if (areValidationErrors(req, res)) return 55 if (areValidationErrors(req, res)) return
13 56
@@ -18,5 +61,6 @@ const searchValidator = [
18// --------------------------------------------------------------------------- 61// ---------------------------------------------------------------------------
19 62
20export { 63export {
64 commonVideosFiltersValidator,
21 searchValidator 65 searchValidator
22} 66}
diff --git a/server/models/utils.ts b/server/models/utils.ts
index 49d32c24f..393f8f036 100644
--- a/server/models/utils.ts
+++ b/server/models/utils.ts
@@ -14,7 +14,7 @@ function getSort (value: string, lastSort: string[] = [ 'id', 'ASC' ]) {
14 } 14 }
15 15
16 // Alias 16 // Alias
17 if (field.toLowerCase() === 'bestmatch') field = Sequelize.col('similarity') 17 if (field.toLowerCase() === 'match') field = Sequelize.col('similarity')
18 18
19 return [ [ field, direction ], lastSort ] 19 return [ [ field, direction ], lastSort ]
20} 20}
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 15b4dda5b..68116e309 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -93,6 +93,7 @@ import { VideoShareModel } from './video-share'
93import { VideoTagModel } from './video-tag' 93import { VideoTagModel } from './video-tag'
94import { ScheduleVideoUpdateModel } from './schedule-video-update' 94import { ScheduleVideoUpdateModel } from './schedule-video-update'
95import { VideoCaptionModel } from './video-caption' 95import { VideoCaptionModel } from './video-caption'
96import { VideosSearchQuery } from '../../../shared/models/search'
96 97
97// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation 98// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
98const indexes: Sequelize.DefineIndexesOptions[] = [ 99const indexes: Sequelize.DefineIndexesOptions[] = [
@@ -133,16 +134,22 @@ export enum ScopeNames {
133 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE' 134 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE'
134} 135}
135 136
137type AvailableForListOptions = {
138 actorId: number,
139 filter?: VideoFilter,
140 categoryOneOf?: number[],
141 nsfw?: boolean,
142 licenceOneOf?: number[],
143 languageOneOf?: string[],
144 tagsOneOf?: string[],
145 tagsAllOf?: string[],
146 withFiles?: boolean,
147 accountId?: number,
148 videoChannelId?: number
149}
150
136@Scopes({ 151@Scopes({
137 [ScopeNames.AVAILABLE_FOR_LIST]: (options: { 152 [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => {
138 actorId: number,
139 hideNSFW: boolean,
140 filter?: VideoFilter,
141 category?: number,
142 withFiles?: boolean,
143 accountId?: number,
144 videoChannelId?: number
145 }) => {
146 const accountInclude = { 153 const accountInclude = {
147 attributes: [ 'id', 'name' ], 154 attributes: [ 'id', 'name' ],
148 model: AccountModel.unscoped(), 155 model: AccountModel.unscoped(),
@@ -243,13 +250,55 @@ export enum ScopeNames {
243 }) 250 })
244 } 251 }
245 252
246 // Hide nsfw videos? 253 // FIXME: issues with sequelize count when making a join on n:m relation, so we just make a IN()
247 if (options.hideNSFW === true) { 254 if (options.tagsAllOf || options.tagsOneOf) {
248 query.where['nsfw'] = false 255 const createTagsIn = (tags: string[]) => {
256 return tags.map(t => VideoModel.sequelize.escape(t))
257 .join(', ')
258 }
259
260 if (options.tagsOneOf) {
261 query.where['id'][Sequelize.Op.in] = Sequelize.literal(
262 '(' +
263 'SELECT "videoId" FROM "videoTag" ' +
264 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
265 'WHERE "tag"."name" IN (' + createTagsIn(options.tagsOneOf) + ')' +
266 ')'
267 )
268 }
269
270 if (options.tagsAllOf) {
271 query.where['id'][Sequelize.Op.in] = Sequelize.literal(
272 '(' +
273 'SELECT "videoId" FROM "videoTag" ' +
274 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
275 'WHERE "tag"."name" IN (' + createTagsIn(options.tagsAllOf) + ')' +
276 'GROUP BY "videoTag"."videoId" HAVING COUNT(*) = ' + options.tagsAllOf.length +
277 ')'
278 )
279 }
280 }
281
282 if (options.nsfw === true || options.nsfw === false) {
283 query.where['nsfw'] = options.nsfw
284 }
285
286 if (options.categoryOneOf) {
287 query.where['category'] = {
288 [Sequelize.Op.or]: options.categoryOneOf
289 }
290 }
291
292 if (options.licenceOneOf) {
293 query.where['licence'] = {
294 [Sequelize.Op.or]: options.licenceOneOf
295 }
249 } 296 }
250 297
251 if (options.category) { 298 if (options.languageOneOf) {
252 query.where['category'] = options.category 299 query.where['language'] = {
300 [Sequelize.Op.or]: options.languageOneOf
301 }
253 } 302 }
254 303
255 if (options.accountId) { 304 if (options.accountId) {
@@ -756,9 +805,13 @@ export class VideoModel extends Model<VideoModel> {
756 start: number, 805 start: number,
757 count: number, 806 count: number,
758 sort: string, 807 sort: string,
759 hideNSFW: boolean, 808 nsfw: boolean,
760 withFiles: boolean, 809 withFiles: boolean,
761 category?: number, 810 categoryOneOf?: number[],
811 licenceOneOf?: number[],
812 languageOneOf?: string[],
813 tagsOneOf?: string[],
814 tagsAllOf?: string[],
762 filter?: VideoFilter, 815 filter?: VideoFilter,
763 accountId?: number, 816 accountId?: number,
764 videoChannelId?: number 817 videoChannelId?: number
@@ -774,13 +827,17 @@ export class VideoModel extends Model<VideoModel> {
774 method: [ 827 method: [
775 ScopeNames.AVAILABLE_FOR_LIST, { 828 ScopeNames.AVAILABLE_FOR_LIST, {
776 actorId: serverActor.id, 829 actorId: serverActor.id,
777 hideNSFW: options.hideNSFW, 830 nsfw: options.nsfw,
778 category: options.category, 831 categoryOneOf: options.categoryOneOf,
832 licenceOneOf: options.licenceOneOf,
833 languageOneOf: options.languageOneOf,
834 tagsOneOf: options.tagsOneOf,
835 tagsAllOf: options.tagsAllOf,
779 filter: options.filter, 836 filter: options.filter,
780 withFiles: options.withFiles, 837 withFiles: options.withFiles,
781 accountId: options.accountId, 838 accountId: options.accountId,
782 videoChannelId: options.videoChannelId 839 videoChannelId: options.videoChannelId
783 } 840 } as AvailableForListOptions
784 ] 841 ]
785 } 842 }
786 843
@@ -794,15 +851,39 @@ export class VideoModel extends Model<VideoModel> {
794 }) 851 })
795 } 852 }
796 853
797 static async searchAndPopulateAccountAndServer (value: string, start: number, count: number, sort: string, hideNSFW: boolean) { 854 static async searchAndPopulateAccountAndServer (options: VideosSearchQuery) {
855 const whereAnd = [ ]
856
857 if (options.startDate || options.endDate) {
858 const publishedAtRange = { }
859
860 if (options.startDate) publishedAtRange[Sequelize.Op.gte] = options.startDate
861 if (options.endDate) publishedAtRange[Sequelize.Op.lte] = options.endDate
862
863 whereAnd.push({ publishedAt: publishedAtRange })
864 }
865
866 if (options.durationMin || options.durationMax) {
867 const durationRange = { }
868
869 if (options.durationMin) durationRange[Sequelize.Op.gte] = options.durationMin
870 if (options.durationMax) durationRange[Sequelize.Op.lte] = options.durationMax
871
872 whereAnd.push({ duration: durationRange })
873 }
874
875 whereAnd.push(createSearchTrigramQuery('VideoModel.name', options.search))
876
798 const query: IFindOptions<VideoModel> = { 877 const query: IFindOptions<VideoModel> = {
799 attributes: { 878 attributes: {
800 include: [ createSimilarityAttribute('VideoModel.name', value) ] 879 include: [ createSimilarityAttribute('VideoModel.name', options.search) ]
801 }, 880 },
802 offset: start, 881 offset: options.start,
803 limit: count, 882 limit: options.count,
804 order: getSort(sort), 883 order: getSort(options.sort),
805 where: createSearchTrigramQuery('VideoModel.name', value) 884 where: {
885 [ Sequelize.Op.and ]: whereAnd
886 }
806 } 887 }
807 888
808 const serverActor = await getServerActor() 889 const serverActor = await getServerActor()
@@ -810,8 +891,13 @@ export class VideoModel extends Model<VideoModel> {
810 method: [ 891 method: [
811 ScopeNames.AVAILABLE_FOR_LIST, { 892 ScopeNames.AVAILABLE_FOR_LIST, {
812 actorId: serverActor.id, 893 actorId: serverActor.id,
813 hideNSFW 894 nsfw: options.nsfw,
814 } 895 categoryOneOf: options.categoryOneOf,
896 licenceOneOf: options.licenceOneOf,
897 languageOneOf: options.languageOneOf,
898 tagsOneOf: options.tagsOneOf,
899 tagsAllOf: options.tagsAllOf
900 } as AvailableForListOptions
815 ] 901 ]
816 } 902 }
817 903
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts
index c0e0302df..820dde889 100644
--- a/server/tests/api/check-params/index.ts
+++ b/server/tests/api/check-params/index.ts
@@ -10,3 +10,4 @@ import './video-captions'
10import './video-channels' 10import './video-channels'
11import './video-comments' 11import './video-comments'
12import './videos' 12import './videos'
13import './search'
diff --git a/server/tests/api/check-params/search.ts b/server/tests/api/check-params/search.ts
new file mode 100644
index 000000000..d35eac7fe
--- /dev/null
+++ b/server/tests/api/check-params/search.ts
@@ -0,0 +1,122 @@
1/* tslint:disable:no-unused-expression */
2
3import 'mocha'
4
5import { flushTests, immutableAssign, killallServers, makeGetRequest, runServer, ServerInfo } from '../../utils'
6import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params'
7
8describe('Test videos API validator', function () {
9 const path = '/api/v1/search/videos/'
10 let server: ServerInfo
11
12 // ---------------------------------------------------------------
13
14 before(async function () {
15 this.timeout(30000)
16
17 await flushTests()
18
19 server = await runServer(1)
20 })
21
22 describe('When searching videos', function () {
23 const query = {
24 search: 'coucou'
25 }
26
27 it('Should fail with a bad start pagination', async function () {
28 await checkBadStartPagination(server.url, path, null, query)
29 })
30
31 it('Should fail with a bad count pagination', async function () {
32 await checkBadCountPagination(server.url, path, null, query)
33 })
34
35 it('Should fail with an incorrect sort', async function () {
36 await checkBadSortPagination(server.url, path, null, query)
37 })
38
39 it('Should success with the correct parameters', async function () {
40 await makeGetRequest({ url: server.url, path, query, statusCodeExpected: 200 })
41 })
42
43 it('Should fail with an invalid category', async function () {
44 const customQuery1 = immutableAssign(query, { categoryOneOf: [ 'aa', 'b' ] })
45 await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: 400 })
46
47 const customQuery2 = immutableAssign(query, { categoryOneOf: 'a' })
48 await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: 400 })
49 })
50
51 it('Should succeed with a valid category', async function () {
52 const customQuery1 = immutableAssign(query, { categoryOneOf: [ 1, 7 ] })
53 await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: 200 })
54
55 const customQuery2 = immutableAssign(query, { categoryOneOf: 1 })
56 await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: 200 })
57 })
58
59 it('Should fail with an invalid licence', async function () {
60 const customQuery1 = immutableAssign(query, { licenceOneOf: [ 'aa', 'b' ] })
61 await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: 400 })
62
63 const customQuery2 = immutableAssign(query, { licenceOneOf: 'a' })
64 await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: 400 })
65 })
66
67 it('Should succeed with a valid licence', async function () {
68 const customQuery1 = immutableAssign(query, { licenceOneOf: [ 1, 2 ] })
69 await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: 200 })
70
71 const customQuery2 = immutableAssign(query, { licenceOneOf: 1 })
72 await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: 200 })
73 })
74
75 it('Should succeed with a valid language', async function () {
76 const customQuery1 = immutableAssign(query, { languageOneOf: [ 'fr', 'en' ] })
77 await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: 200 })
78
79 const customQuery2 = immutableAssign(query, { languageOneOf: 'fr' })
80 await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: 200 })
81 })
82
83 it('Should succeed with valid tags', async function () {
84 const customQuery1 = immutableAssign(query, { tagsOneOf: [ 'tag1', 'tag2' ] })
85 await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: 200 })
86
87 const customQuery2 = immutableAssign(query, { tagsOneOf: 'tag1' })
88 await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: 200 })
89
90 const customQuery3 = immutableAssign(query, { tagsAllOf: [ 'tag1', 'tag2' ] })
91 await makeGetRequest({ url: server.url, path, query: customQuery3, statusCodeExpected: 200 })
92
93 const customQuery4 = immutableAssign(query, { tagsAllOf: 'tag1' })
94 await makeGetRequest({ url: server.url, path, query: customQuery4, statusCodeExpected: 200 })
95 })
96
97 it('Should fail with invalid durations', async function () {
98 const customQuery1 = immutableAssign(query, { durationMin: 'hello' })
99 await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: 400 })
100
101 const customQuery2 = immutableAssign(query, { durationMax: 'hello' })
102 await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: 400 })
103 })
104
105 it('Should fail with invalid dates', async function () {
106 const customQuery1 = immutableAssign(query, { startDate: 'hello' })
107 await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: 400 })
108
109 const customQuery2 = immutableAssign(query, { endDate: 'hello' })
110 await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: 400 })
111 })
112 })
113
114 after(async function () {
115 killallServers([ server ])
116
117 // Keep the logs if the test failed
118 if (this['ok']) {
119 await flushTests()
120 }
121 })
122})
diff --git a/server/tests/api/index-fast.ts b/server/tests/api/index-fast.ts
index d530dfc06..531a09b82 100644
--- a/server/tests/api/index-fast.ts
+++ b/server/tests/api/index-fast.ts
@@ -14,3 +14,4 @@ import './videos/services'
14import './server/email' 14import './server/email'
15import './server/config' 15import './server/config'
16import './server/reverse-proxy' 16import './server/reverse-proxy'
17import './search/search-videos'
diff --git a/server/tests/api/search/search-videos.ts b/server/tests/api/search/search-videos.ts
new file mode 100644
index 000000000..7fc133b46
--- /dev/null
+++ b/server/tests/api/search/search-videos.ts
@@ -0,0 +1,299 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import {
6 advancedVideosSearch,
7 flushTests,
8 killallServers,
9 runServer,
10 searchVideo,
11 ServerInfo,
12 setAccessTokensToServers,
13 uploadVideo,
14 wait,
15 immutableAssign
16} from '../../utils'
17
18const expect = chai.expect
19
20describe('Test a videos search', function () {
21 let server: ServerInfo = null
22 let startDate: string
23
24 before(async function () {
25 this.timeout(30000)
26
27 await flushTests()
28
29 server = await runServer(1)
30
31 await setAccessTokensToServers([ server ])
32
33 {
34 const attributes1 = {
35 name: '1111 2222 3333',
36 fixture: '60fps_720p_small.mp4', // 2 seconds
37 category: 1,
38 licence: 1,
39 nsfw: false,
40 language: 'fr'
41 }
42 await uploadVideo(server.url, server.accessToken, attributes1)
43
44 const attributes2 = immutableAssign(attributes1, { name: attributes1.name + ' - 2', fixture: 'video_short.mp4' })
45 await uploadVideo(server.url, server.accessToken, attributes2)
46
47 const attributes3 = immutableAssign(attributes1, { name: attributes1.name + ' - 3', language: 'en' })
48 await uploadVideo(server.url, server.accessToken, attributes3)
49
50 const attributes4 = immutableAssign(attributes1, { name: attributes1.name + ' - 4', language: 'pl', nsfw: true })
51 await uploadVideo(server.url, server.accessToken, attributes4)
52
53 await wait(1000)
54
55 startDate = new Date().toISOString()
56
57 const attributes5 = immutableAssign(attributes1, { name: attributes1.name + ' - 5', licence: 2 })
58 await uploadVideo(server.url, server.accessToken, attributes5)
59
60 const attributes6 = immutableAssign(attributes1, { name: attributes1.name + ' - 6', tags: [ 't1', 't2 '] })
61 await uploadVideo(server.url, server.accessToken, attributes6)
62
63 const attributes7 = immutableAssign(attributes1, { name: attributes1.name + ' - 7' })
64 await uploadVideo(server.url, server.accessToken, attributes7)
65
66 const attributes8 = immutableAssign(attributes1, { name: attributes1.name + ' - 8', licence: 4 })
67 await uploadVideo(server.url, server.accessToken, attributes8)
68 }
69
70 {
71 const attributes = {
72 name: '3333 4444 5555',
73 fixture: 'video_short.mp4',
74 category: 2,
75 licence: 2,
76 language: 'en'
77 }
78 await uploadVideo(server.url, server.accessToken, attributes)
79
80 await uploadVideo(server.url, server.accessToken, immutableAssign(attributes, { name: attributes.name + ' duplicate' }))
81 }
82
83 {
84 const attributes = {
85 name: '6666 7777 8888',
86 fixture: 'video_short.mp4',
87 category: 3,
88 licence: 3,
89 language: 'pl'
90 }
91 await uploadVideo(server.url, server.accessToken, attributes)
92 }
93
94 {
95 const attributes1 = {
96 name: '9999',
97 tags: [ 'aaaa', 'bbbb', 'cccc' ],
98 category: 1
99 }
100 await uploadVideo(server.url, server.accessToken, attributes1)
101 await uploadVideo(server.url, server.accessToken, immutableAssign(attributes1, { category: 2 }))
102
103 await uploadVideo(server.url, server.accessToken, immutableAssign(attributes1, { tags: [ 'cccc', 'dddd' ] }))
104 await uploadVideo(server.url, server.accessToken, immutableAssign(attributes1, { tags: [ 'eeee', 'ffff' ] }))
105 }
106 })
107
108 it('Should make a simple search and not have results', async function () {
109 const res = await searchVideo(server.url, 'abc')
110
111 expect(res.body.total).to.equal(0)
112 expect(res.body.data).to.have.lengthOf(0)
113 })
114
115 it('Should make a simple search and have results', async function () {
116 const res = await searchVideo(server.url, '4444 5555 duplicate')
117
118 expect(res.body.total).to.equal(2)
119
120 const videos = res.body.data
121 expect(videos).to.have.lengthOf(2)
122
123 // bestmatch
124 expect(videos[0].name).to.equal('3333 4444 5555 duplicate')
125 expect(videos[1].name).to.equal('3333 4444 5555')
126 })
127
128 it('Should search by tags (one of)', async function () {
129 const query = {
130 search: '9999',
131 categoryOneOf: [ 1 ],
132 tagsOneOf: [ 'aaaa', 'ffff' ]
133 }
134 const res1 = await advancedVideosSearch(server.url, query)
135 expect(res1.body.total).to.equal(2)
136
137 const res2 = await advancedVideosSearch(server.url, immutableAssign(query, { tagsOneOf: [ 'blabla' ] }))
138 expect(res2.body.total).to.equal(0)
139 })
140
141 it('Should search by tags (all of)', async function () {
142 const query = {
143 search: '9999',
144 categoryOneOf: [ 1 ],
145 tagsAllOf: [ 'cccc' ]
146 }
147 const res1 = await advancedVideosSearch(server.url, query)
148 expect(res1.body.total).to.equal(2)
149
150 const res2 = await advancedVideosSearch(server.url, immutableAssign(query, { tagsAllOf: [ 'blabla' ] }))
151 expect(res2.body.total).to.equal(0)
152
153 const res3 = await advancedVideosSearch(server.url, immutableAssign(query, { tagsAllOf: [ 'bbbb', 'cccc' ] }))
154 expect(res3.body.total).to.equal(1)
155 })
156
157 it('Should search by category', async function () {
158 const query = {
159 search: '6666',
160 categoryOneOf: [ 3 ]
161 }
162 const res1 = await advancedVideosSearch(server.url, query)
163 expect(res1.body.total).to.equal(1)
164 expect(res1.body.data[0].name).to.equal('6666 7777 8888')
165
166 const res2 = await advancedVideosSearch(server.url, immutableAssign(query, { categoryOneOf: [ 2 ] }))
167 expect(res2.body.total).to.equal(0)
168 })
169
170 it('Should search by licence', async function () {
171 const query = {
172 search: '4444 5555',
173 licenceOneOf: [ 2 ]
174 }
175 const res1 = await advancedVideosSearch(server.url, query)
176 expect(res1.body.total).to.equal(2)
177 expect(res1.body.data[0].name).to.equal('3333 4444 5555')
178 expect(res1.body.data[1].name).to.equal('3333 4444 5555 duplicate')
179
180 const res2 = await advancedVideosSearch(server.url, immutableAssign(query, { licenceOneOf: [ 3 ] }))
181 expect(res2.body.total).to.equal(0)
182 })
183
184 it('Should search by languages', async function () {
185 const query = {
186 search: '1111 2222 3333',
187 languageOneOf: [ 'pl', 'en' ]
188 }
189 const res1 = await advancedVideosSearch(server.url, query)
190 expect(res1.body.total).to.equal(2)
191 expect(res1.body.data[0].name).to.equal('1111 2222 3333 - 3')
192 expect(res1.body.data[1].name).to.equal('1111 2222 3333 - 4')
193
194 const res2 = await advancedVideosSearch(server.url, immutableAssign(query, { languageOneOf: [ 'eo' ] }))
195 expect(res2.body.total).to.equal(0)
196 })
197
198 it('Should search by start date', async function () {
199 const query = {
200 search: '1111 2222 3333',
201 startDate
202 }
203
204 const res = await advancedVideosSearch(server.url, query)
205 expect(res.body.total).to.equal(4)
206
207 const videos = res.body.data
208 expect(videos[0].name).to.equal('1111 2222 3333 - 5')
209 expect(videos[1].name).to.equal('1111 2222 3333 - 6')
210 expect(videos[2].name).to.equal('1111 2222 3333 - 7')
211 expect(videos[3].name).to.equal('1111 2222 3333 - 8')
212 })
213
214 it('Should make an advanced search', async function () {
215 const query = {
216 search: '1111 2222 3333',
217 languageOneOf: [ 'pl', 'fr' ],
218 durationMax: 4,
219 nsfw: false,
220 licenceOneOf: [ 1, 4 ]
221 }
222
223 const res = await advancedVideosSearch(server.url, query)
224 expect(res.body.total).to.equal(4)
225
226 const videos = res.body.data
227 expect(videos[0].name).to.equal('1111 2222 3333')
228 expect(videos[1].name).to.equal('1111 2222 3333 - 6')
229 expect(videos[2].name).to.equal('1111 2222 3333 - 7')
230 expect(videos[3].name).to.equal('1111 2222 3333 - 8')
231 })
232
233 it('Should make an advanced search and sort results', async function () {
234 const query = {
235 search: '1111 2222 3333',
236 languageOneOf: [ 'pl', 'fr' ],
237 durationMax: 4,
238 nsfw: false,
239 licenceOneOf: [ 1, 4 ],
240 sort: '-name'
241 }
242
243 const res = await advancedVideosSearch(server.url, query)
244 expect(res.body.total).to.equal(4)
245
246 const videos = res.body.data
247 expect(videos[0].name).to.equal('1111 2222 3333 - 8')
248 expect(videos[1].name).to.equal('1111 2222 3333 - 7')
249 expect(videos[2].name).to.equal('1111 2222 3333 - 6')
250 expect(videos[3].name).to.equal('1111 2222 3333')
251 })
252
253 it('Should make an advanced search and only show the first result', async function () {
254 const query = {
255 search: '1111 2222 3333',
256 languageOneOf: [ 'pl', 'fr' ],
257 durationMax: 4,
258 nsfw: false,
259 licenceOneOf: [ 1, 4 ],
260 sort: '-name',
261 start: 0,
262 count: 1
263 }
264
265 const res = await advancedVideosSearch(server.url, query)
266 expect(res.body.total).to.equal(4)
267
268 const videos = res.body.data
269 expect(videos[0].name).to.equal('1111 2222 3333 - 8')
270 })
271
272 it('Should make an advanced search and only show the last result', async function () {
273 const query = {
274 search: '1111 2222 3333',
275 languageOneOf: [ 'pl', 'fr' ],
276 durationMax: 4,
277 nsfw: false,
278 licenceOneOf: [ 1, 4 ],
279 sort: '-name',
280 start: 3,
281 count: 1
282 }
283
284 const res = await advancedVideosSearch(server.url, query)
285 expect(res.body.total).to.equal(4)
286
287 const videos = res.body.data
288 expect(videos[0].name).to.equal('1111 2222 3333')
289 })
290
291 after(async function () {
292 killallServers([ server ])
293
294 // Keep the logs if the test failed
295 if (this['ok']) {
296 await flushTests()
297 }
298 })
299})
diff --git a/server/tests/api/videos/single-server.ts b/server/tests/api/videos/single-server.ts
index d8af94e8f..ba4920d1b 100644
--- a/server/tests/api/videos/single-server.ts
+++ b/server/tests/api/videos/single-server.ts
@@ -16,13 +16,11 @@ import {
16 getVideosList, 16 getVideosList,
17 getVideosListPagination, 17 getVideosListPagination,
18 getVideosListSort, 18 getVideosListSort,
19 getVideosWithFilters,
19 killallServers, 20 killallServers,
20 rateVideo, 21 rateVideo,
21 removeVideo, 22 removeVideo,
22 runServer, 23 runServer,
23 searchVideo,
24 searchVideoWithPagination,
25 searchVideoWithSort,
26 ServerInfo, 24 ServerInfo,
27 setAccessTokensToServers, 25 setAccessTokensToServers,
28 testImage, 26 testImage,
@@ -218,72 +216,6 @@ describe('Test a single server', function () {
218 expect(video.views).to.equal(3) 216 expect(video.views).to.equal(3)
219 }) 217 })
220 218
221 it('Should search the video by name', async function () {
222 const res = await searchVideo(server.url, 'my')
223
224 expect(res.body.total).to.equal(1)
225 expect(res.body.data).to.be.an('array')
226 expect(res.body.data.length).to.equal(1)
227
228 const video = res.body.data[0]
229 await completeVideoCheck(server.url, video, getCheckAttributes)
230 })
231
232 // Not implemented yet
233 // it('Should search the video by tag', async function () {
234 // const res = await searchVideo(server.url, 'tag1')
235 //
236 // expect(res.body.total).to.equal(1)
237 // expect(res.body.data).to.be.an('array')
238 // expect(res.body.data.length).to.equal(1)
239 //
240 // const video = res.body.data[0]
241 // expect(video.name).to.equal('my super name')
242 // expect(video.category).to.equal(2)
243 // expect(video.categoryLabel).to.equal('Films')
244 // expect(video.licence).to.equal(6)
245 // expect(video.licenceLabel).to.equal('Attribution - Non Commercial - No Derivatives')
246 // expect(video.language).to.equal('zh')
247 // expect(video.languageLabel).to.equal('Chinese')
248 // expect(video.nsfw).to.be.ok
249 // expect(video.description).to.equal('my super description')
250 // expect(video.account.name).to.equal('root')
251 // expect(video.account.host).to.equal('localhost:9001')
252 // expect(video.isLocal).to.be.true
253 // expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ])
254 // expect(dateIsValid(video.createdAt)).to.be.true
255 // expect(dateIsValid(video.updatedAt)).to.be.true
256 //
257 // const test = await testVideoImage(server.url, 'video_short.webm', video.thumbnailPath)
258 // expect(test).to.equal(true)
259 // })
260
261 it('Should not find a search by name', async function () {
262 const res = await searchVideo(server.url, 'hello')
263
264 expect(res.body.total).to.equal(0)
265 expect(res.body.data).to.be.an('array')
266 expect(res.body.data.length).to.equal(0)
267 })
268
269 // Not implemented yet
270 // it('Should not find a search by author', async function () {
271 // const res = await searchVideo(server.url, 'hello')
272 //
273 // expect(res.body.total).to.equal(0)
274 // expect(res.body.data).to.be.an('array')
275 // expect(res.body.data.length).to.equal(0)
276 // })
277 //
278 // Not implemented yet
279 // it('Should not find a search by tag', async function () {
280 // const res = await searchVideo(server.url, 'hello')
281 //
282 // expect(res.body.total).to.equal(0)
283 // expect(res.body.data).to.be.an('array')
284 // expect(res.body.data.length).to.equal(0)
285 // })
286
287 it('Should remove the video', async function () { 219 it('Should remove the video', async function () {
288 await removeVideo(server.url, server.accessToken, videoId) 220 await removeVideo(server.url, server.accessToken, videoId)
289 221
@@ -386,65 +318,6 @@ describe('Test a single server', function () {
386 expect(videos[0].name).to.equal(videosListBase[5].name) 318 expect(videos[0].name).to.equal(videosListBase[5].name)
387 }) 319 })
388 320
389 it('Should search the first video', async function () {
390 const res = await searchVideoWithPagination(server.url, 'webm', 0, 1, 'name')
391
392 const videos = res.body.data
393 expect(res.body.total).to.equal(4)
394 expect(videos.length).to.equal(1)
395 expect(videos[0].name).to.equal('video_short1.webm name')
396 })
397
398 it('Should search the last two videos', async function () {
399 const res = await searchVideoWithPagination(server.url, 'webm', 2, 2, 'name')
400
401 const videos = res.body.data
402 expect(res.body.total).to.equal(4)
403 expect(videos.length).to.equal(2)
404 expect(videos[0].name).to.equal('video_short3.webm name')
405 expect(videos[1].name).to.equal('video_short.webm name')
406 })
407
408 it('Should search all the webm videos', async function () {
409 const res = await searchVideoWithPagination(server.url, 'webm', 0, 15)
410
411 const videos = res.body.data
412 expect(res.body.total).to.equal(4)
413 expect(videos.length).to.equal(4)
414 })
415
416 // Not implemented yet
417 // it('Should search all the root author videos', async function () {
418 // const res = await searchVideoWithPagination(server.url, 'root', 0, 15)
419 //
420 // const videos = res.body.data
421 // expect(res.body.total).to.equal(6)
422 // expect(videos.length).to.equal(6)
423 // })
424
425 // Not implemented yet
426 // it('Should search all the 9001 port videos', async function () {
427 // const res = await videosUtils.searchVideoWithPagination(server.url, '9001', 'host', 0, 15)
428
429 // const videos = res.body.data
430 // expect(res.body.total).to.equal(6)
431 // expect(videos.length).to.equal(6)
432
433 // done()
434 // })
435 // })
436
437 // it('Should search all the localhost videos', async function () {
438 // const res = await videosUtils.searchVideoWithPagination(server.url, 'localhost', 'host', 0, 15)
439
440 // const videos = res.body.data
441 // expect(res.body.total).to.equal(6)
442 // expect(videos.length).to.equal(6)
443
444 // done()
445 // })
446 // })
447
448 it('Should list and sort by name in descending order', async function () { 321 it('Should list and sort by name in descending order', async function () {
449 const res = await getVideosListSort(server.url, '-name') 322 const res = await getVideosListSort(server.url, '-name')
450 323
@@ -457,21 +330,8 @@ describe('Test a single server', function () {
457 expect(videos[3].name).to.equal('video_short3.webm name') 330 expect(videos[3].name).to.equal('video_short3.webm name')
458 expect(videos[4].name).to.equal('video_short2.webm name') 331 expect(videos[4].name).to.equal('video_short2.webm name')
459 expect(videos[5].name).to.equal('video_short1.webm name') 332 expect(videos[5].name).to.equal('video_short1.webm name')
460 })
461
462 it('Should search and sort by name in ascending order', async function () {
463 const res = await searchVideoWithSort(server.url, 'webm', 'name')
464 333
465 const videos = res.body.data 334 videoId = videos[3].uuid
466 expect(res.body.total).to.equal(4)
467 expect(videos.length).to.equal(4)
468
469 expect(videos[0].name).to.equal('video_short1.webm name')
470 expect(videos[1].name).to.equal('video_short2.webm name')
471 expect(videos[2].name).to.equal('video_short3.webm name')
472 expect(videos[3].name).to.equal('video_short.webm name')
473
474 videoId = videos[2].id
475 }) 335 })
476 336
477 it('Should update a video', async function () { 337 it('Should update a video', async function () {
@@ -488,6 +348,15 @@ describe('Test a single server', function () {
488 await updateVideo(server.url, server.accessToken, videoId, attributes) 348 await updateVideo(server.url, server.accessToken, videoId, attributes)
489 }) 349 })
490 350
351 it('Should filter by tags and category', async function () {
352 const res1 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: 4 })
353 expect(res1.body.total).to.equal(1)
354 expect(res1.body.data[0].name).to.equal('my super video updated')
355
356 const res2 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: 3 })
357 expect(res2.body.total).to.equal(0)
358 })
359
491 it('Should have the video updated', async function () { 360 it('Should have the video updated', async function () {
492 this.timeout(60000) 361 this.timeout(60000)
493 362
diff --git a/server/tests/api/videos/video-nsfw.ts b/server/tests/api/videos/video-nsfw.ts
index 6af0ca8af..38bdaa54e 100644
--- a/server/tests/api/videos/video-nsfw.ts
+++ b/server/tests/api/videos/video-nsfw.ts
@@ -30,7 +30,7 @@ describe('Test video NSFW policy', function () {
30 let userAccessToken: string 30 let userAccessToken: string
31 let customConfig: CustomConfig 31 let customConfig: CustomConfig
32 32
33 function getVideosFunctions (token?: string) { 33 function getVideosFunctions (token?: string, query = {}) {
34 return getMyUserInformation(server.url, server.accessToken) 34 return getMyUserInformation(server.url, server.accessToken)
35 .then(res => { 35 .then(res => {
36 const user: User = res.body 36 const user: User = res.body
@@ -39,10 +39,10 @@ describe('Test video NSFW policy', function () {
39 39
40 if (token) { 40 if (token) {
41 return Promise.all([ 41 return Promise.all([
42 getVideosListWithToken(server.url, token), 42 getVideosListWithToken(server.url, token, query),
43 searchVideoWithToken(server.url, 'n', token), 43 searchVideoWithToken(server.url, 'n', token, query),
44 getAccountVideos(server.url, token, accountName, 0, 5), 44 getAccountVideos(server.url, token, accountName, 0, 5, undefined, query),
45 getVideoChannelVideos(server.url, token, videoChannelUUID, 0, 5) 45 getVideoChannelVideos(server.url, token, videoChannelUUID, 0, 5, undefined, query)
46 ]) 46 ])
47 } 47 }
48 48
@@ -200,6 +200,26 @@ describe('Test video NSFW policy', function () {
200 expect(videos[ 0 ].name).to.equal('normal') 200 expect(videos[ 0 ].name).to.equal('normal')
201 expect(videos[ 1 ].name).to.equal('nsfw') 201 expect(videos[ 1 ].name).to.equal('nsfw')
202 }) 202 })
203
204 it('Should display NSFW videos when the nsfw param === true', async function () {
205 for (const res of await getVideosFunctions(server.accessToken, { nsfw: true })) {
206 expect(res.body.total).to.equal(1)
207
208 const videos = res.body.data
209 expect(videos).to.have.lengthOf(1)
210 expect(videos[ 0 ].name).to.equal('nsfw')
211 }
212 })
213
214 it('Should hide NSFW videos when the nsfw param === true', async function () {
215 for (const res of await getVideosFunctions(server.accessToken, { nsfw: false })) {
216 expect(res.body.total).to.equal(1)
217
218 const videos = res.body.data
219 expect(videos).to.have.lengthOf(1)
220 expect(videos[ 0 ].name).to.equal('normal')
221 }
222 })
203 }) 223 })
204 224
205 after(async function () { 225 after(async function () {
diff --git a/server/tests/utils/index.ts b/server/tests/utils/index.ts
index 5b560ca39..391db18cf 100644
--- a/server/tests/utils/index.ts
+++ b/server/tests/utils/index.ts
@@ -14,3 +14,4 @@ export * from './videos/video-blacklist'
14export * from './videos/video-channels' 14export * from './videos/video-channels'
15export * from './videos/videos' 15export * from './videos/videos'
16export * from './feeds/feeds' 16export * from './feeds/feeds'
17export * from './search/videos'
diff --git a/server/tests/utils/requests/check-api-params.ts b/server/tests/utils/requests/check-api-params.ts
index 7550eb3d8..edb47e0e9 100644
--- a/server/tests/utils/requests/check-api-params.ts
+++ b/server/tests/utils/requests/check-api-params.ts
@@ -1,31 +1,32 @@
1import { makeGetRequest } from './requests' 1import { makeGetRequest } from './requests'
2import { immutableAssign } from '..'
2 3
3function checkBadStartPagination (url: string, path: string, token?: string) { 4function checkBadStartPagination (url: string, path: string, token?: string, query = {}) {
4 return makeGetRequest({ 5 return makeGetRequest({
5 url, 6 url,
6 path, 7 path,
7 token, 8 token,
8 query: { start: 'hello' }, 9 query: immutableAssign(query, { start: 'hello' }),
9 statusCodeExpected: 400 10 statusCodeExpected: 400
10 }) 11 })
11} 12}
12 13
13function checkBadCountPagination (url: string, path: string, token?: string) { 14function checkBadCountPagination (url: string, path: string, token?: string, query = {}) {
14 return makeGetRequest({ 15 return makeGetRequest({
15 url, 16 url,
16 path, 17 path,
17 token, 18 token,
18 query: { count: 'hello' }, 19 query: immutableAssign(query, { count: 'hello' }),
19 statusCodeExpected: 400 20 statusCodeExpected: 400
20 }) 21 })
21} 22}
22 23
23function checkBadSortPagination (url: string, path: string, token?: string) { 24function checkBadSortPagination (url: string, path: string, token?: string, query = {}) {
24 return makeGetRequest({ 25 return makeGetRequest({
25 url, 26 url,
26 path, 27 path,
27 token, 28 token,
28 query: { sort: 'hello' }, 29 query: immutableAssign(query, { sort: 'hello' }),
29 statusCodeExpected: 400 30 statusCodeExpected: 400
30 }) 31 })
31} 32}
diff --git a/server/tests/utils/search/videos.ts b/server/tests/utils/search/videos.ts
new file mode 100644
index 000000000..3a0c10e42
--- /dev/null
+++ b/server/tests/utils/search/videos.ts
@@ -0,0 +1,77 @@
1/* tslint:disable:no-unused-expression */
2
3import * as request from 'supertest'
4import { VideosSearchQuery } from '../../../../shared/models/search'
5import { immutableAssign } from '..'
6
7function searchVideo (url: string, search: string) {
8 const path = '/api/v1/search/videos'
9 const req = request(url)
10 .get(path)
11 .query({ sort: '-publishedAt', search })
12 .set('Accept', 'application/json')
13
14 return req.expect(200)
15 .expect('Content-Type', /json/)
16}
17
18function searchVideoWithToken (url: string, search: string, token: string, query: { nsfw?: boolean } = {}) {
19 const path = '/api/v1/search/videos'
20 const req = request(url)
21 .get(path)
22 .set('Authorization', 'Bearer ' + token)
23 .query(immutableAssign(query, { sort: '-publishedAt', search }))
24 .set('Accept', 'application/json')
25
26 return req.expect(200)
27 .expect('Content-Type', /json/)
28}
29
30function searchVideoWithPagination (url: string, search: string, start: number, count: number, sort?: string) {
31 const path = '/api/v1/search/videos'
32
33 const req = request(url)
34 .get(path)
35 .query({ start })
36 .query({ search })
37 .query({ count })
38
39 if (sort) req.query({ sort })
40
41 return req.set('Accept', 'application/json')
42 .expect(200)
43 .expect('Content-Type', /json/)
44}
45
46function searchVideoWithSort (url: string, search: string, sort: string) {
47 const path = '/api/v1/search/videos'
48
49 return request(url)
50 .get(path)
51 .query({ search })
52 .query({ sort })
53 .set('Accept', 'application/json')
54 .expect(200)
55 .expect('Content-Type', /json/)
56}
57
58function advancedVideosSearch (url: string, options: VideosSearchQuery) {
59 const path = '/api/v1/search/videos'
60
61 return request(url)
62 .get(path)
63 .query(options)
64 .set('Accept', 'application/json')
65 .expect(200)
66 .expect('Content-Type', /json/)
67}
68
69// ---------------------------------------------------------------------------
70
71export {
72 searchVideo,
73 advancedVideosSearch,
74 searchVideoWithToken,
75 searchVideoWithPagination,
76 searchVideoWithSort
77}
diff --git a/server/tests/utils/videos/videos.ts b/server/tests/utils/videos/videos.ts
index a42d0f043..8c49eb02b 100644
--- a/server/tests/utils/videos/videos.ts
+++ b/server/tests/utils/videos/videos.ts
@@ -7,7 +7,7 @@ import { extname, join } from 'path'
7import * as request from 'supertest' 7import * as request from 'supertest'
8import { 8import {
9 buildAbsoluteFixturePath, 9 buildAbsoluteFixturePath,
10 getMyUserInformation, 10 getMyUserInformation, immutableAssign,
11 makeGetRequest, 11 makeGetRequest,
12 makePutBodyRequest, 12 makePutBodyRequest,
13 makeUploadRequest, 13 makeUploadRequest,
@@ -133,13 +133,13 @@ function getVideosList (url: string) {
133 .expect('Content-Type', /json/) 133 .expect('Content-Type', /json/)
134} 134}
135 135
136function getVideosListWithToken (url: string, token: string) { 136function getVideosListWithToken (url: string, token: string, query: { nsfw?: boolean } = {}) {
137 const path = '/api/v1/videos' 137 const path = '/api/v1/videos'
138 138
139 return request(url) 139 return request(url)
140 .get(path) 140 .get(path)
141 .set('Authorization', 'Bearer ' + token) 141 .set('Authorization', 'Bearer ' + token)
142 .query({ sort: 'name' }) 142 .query(immutableAssign(query, { sort: 'name' }))
143 .set('Accept', 'application/json') 143 .set('Accept', 'application/json')
144 .expect(200) 144 .expect(200)
145 .expect('Content-Type', /json/) 145 .expect('Content-Type', /json/)
@@ -172,17 +172,25 @@ function getMyVideos (url: string, accessToken: string, start: number, count: nu
172 .expect('Content-Type', /json/) 172 .expect('Content-Type', /json/)
173} 173}
174 174
175function getAccountVideos (url: string, accessToken: string, accountName: string, start: number, count: number, sort?: string) { 175function getAccountVideos (
176 url: string,
177 accessToken: string,
178 accountName: string,
179 start: number,
180 count: number,
181 sort?: string,
182 query: { nsfw?: boolean } = {}
183) {
176 const path = '/api/v1/accounts/' + accountName + '/videos' 184 const path = '/api/v1/accounts/' + accountName + '/videos'
177 185
178 return makeGetRequest({ 186 return makeGetRequest({
179 url, 187 url,
180 path, 188 path,
181 query: { 189 query: immutableAssign(query, {
182 start, 190 start,
183 count, 191 count,
184 sort 192 sort
185 }, 193 }),
186 token: accessToken, 194 token: accessToken,
187 statusCodeExpected: 200 195 statusCodeExpected: 200
188 }) 196 })
@@ -194,18 +202,19 @@ function getVideoChannelVideos (
194 videoChannelId: number | string, 202 videoChannelId: number | string,
195 start: number, 203 start: number,
196 count: number, 204 count: number,
197 sort?: string 205 sort?: string,
206 query: { nsfw?: boolean } = {}
198) { 207) {
199 const path = '/api/v1/video-channels/' + videoChannelId + '/videos' 208 const path = '/api/v1/video-channels/' + videoChannelId + '/videos'
200 209
201 return makeGetRequest({ 210 return makeGetRequest({
202 url, 211 url,
203 path, 212 path,
204 query: { 213 query: immutableAssign(query, {
205 start, 214 start,
206 count, 215 count,
207 sort 216 sort
208 }, 217 }),
209 token: accessToken, 218 token: accessToken,
210 statusCodeExpected: 200 219 statusCodeExpected: 200
211 }) 220 })
@@ -237,65 +246,25 @@ function getVideosListSort (url: string, sort: string) {
237 .expect('Content-Type', /json/) 246 .expect('Content-Type', /json/)
238} 247}
239 248
240function removeVideo (url: string, token: string, id: number | string, expectedStatus = 204) { 249function getVideosWithFilters (url: string, query: { tagsAllOf: string[], categoryOneOf: number[] | number }) {
241 const path = '/api/v1/videos' 250 const path = '/api/v1/videos'
242 251
243 return request(url) 252 return request(url)
244 .delete(path + '/' + id)
245 .set('Accept', 'application/json')
246 .set('Authorization', 'Bearer ' + token)
247 .expect(expectedStatus)
248}
249
250function searchVideo (url: string, search: string) {
251 const path = '/api/v1/search/videos'
252 const req = request(url)
253 .get(path) 253 .get(path)
254 .query({ search }) 254 .query(query)
255 .set('Accept', 'application/json') 255 .set('Accept', 'application/json')
256 256 .expect(200)
257 return req.expect(200)
258 .expect('Content-Type', /json/) 257 .expect('Content-Type', /json/)
259} 258}
260 259
261function searchVideoWithToken (url: string, search: string, token: string) { 260function removeVideo (url: string, token: string, id: number | string, expectedStatus = 204) {
262 const path = '/api/v1/videos' 261 const path = '/api/v1/videos'
263 const req = request(url)
264 .get(path + '/search')
265 .set('Authorization', 'Bearer ' + token)
266 .query({ search })
267 .set('Accept', 'application/json')
268
269 return req.expect(200)
270 .expect('Content-Type', /json/)
271}
272
273function searchVideoWithPagination (url: string, search: string, start: number, count: number, sort?: string) {
274 const path = '/api/v1/search/videos'
275
276 const req = request(url)
277 .get(path)
278 .query({ start })
279 .query({ search })
280 .query({ count })
281
282 if (sort) req.query({ sort })
283
284 return req.set('Accept', 'application/json')
285 .expect(200)
286 .expect('Content-Type', /json/)
287}
288
289function searchVideoWithSort (url: string, search: string, sort: string) {
290 const path = '/api/v1/search/videos'
291 262
292 return request(url) 263 return request(url)
293 .get(path) 264 .delete(path + '/' + id)
294 .query({ search })
295 .query({ sort })
296 .set('Accept', 'application/json') 265 .set('Accept', 'application/json')
297 .expect(200) 266 .set('Authorization', 'Bearer ' + token)
298 .expect('Content-Type', /json/) 267 .expect(expectedStatus)
299} 268}
300 269
301async function checkVideoFilesWereRemoved (videoUUID: string, serverNumber: number) { 270async function checkVideoFilesWereRemoved (videoUUID: string, serverNumber: number) {
@@ -581,18 +550,15 @@ export {
581 getMyVideos, 550 getMyVideos,
582 getAccountVideos, 551 getAccountVideos,
583 getVideoChannelVideos, 552 getVideoChannelVideos,
584 searchVideoWithToken,
585 getVideo, 553 getVideo,
586 getVideoWithToken, 554 getVideoWithToken,
587 getVideosList, 555 getVideosList,
588 getVideosListPagination, 556 getVideosListPagination,
589 getVideosListSort, 557 getVideosListSort,
590 removeVideo, 558 removeVideo,
591 searchVideo,
592 searchVideoWithPagination,
593 searchVideoWithSort,
594 getVideosListWithToken, 559 getVideosListWithToken,
595 uploadVideo, 560 uploadVideo,
561 getVideosWithFilters,
596 updateVideo, 562 updateVideo,
597 rateVideo, 563 rateVideo,
598 viewVideo, 564 viewVideo,
diff --git a/shared/models/index.ts b/shared/models/index.ts
index c8ce71f17..1db00c295 100644
--- a/shared/models/index.ts
+++ b/shared/models/index.ts
@@ -4,6 +4,7 @@ export * from './users'
4export * from './videos' 4export * from './videos'
5export * from './feeds' 5export * from './feeds'
6export * from './i18n' 6export * from './i18n'
7export * from './search'
7export * from './server/job.model' 8export * from './server/job.model'
8export * from './oauth-client-local.model' 9export * from './oauth-client-local.model'
9export * from './result-list.model' 10export * from './result-list.model'
diff --git a/shared/models/search/index.ts b/shared/models/search/index.ts
new file mode 100644
index 000000000..288ee41ef
--- /dev/null
+++ b/shared/models/search/index.ts
@@ -0,0 +1 @@
export * from './videos-search-query.model'
diff --git a/shared/models/search/videos-search-query.model.ts b/shared/models/search/videos-search-query.model.ts
new file mode 100644
index 000000000..bb23bd636
--- /dev/null
+++ b/shared/models/search/videos-search-query.model.ts
@@ -0,0 +1,24 @@
1export interface VideosSearchQuery {
2 search: string
3
4 start?: number
5 count?: number
6 sort?: string
7
8 startDate?: string // ISO 8601
9 endDate?: string // ISO 8601
10
11 nsfw?: boolean
12
13 categoryOneOf?: number[]
14
15 licenceOneOf?: number[]
16
17 languageOneOf?: string[]
18
19 tagsOneOf?: string[]
20 tagsAllOf?: string[]
21
22 durationMin?: number // seconds
23 durationMax?: number // seconds
24}