Add videos list filters
authorChocobozzz <me@florianbigard.com>
Fri, 20 Jul 2018 12:35:18 +0000 (14:35 +0200)
committerChocobozzz <me@florianbigard.com>
Tue, 24 Jul 2018 12:04:05 +0000 (14:04 +0200)
26 files changed:
server/controllers/api/accounts.ts
server/controllers/api/search.ts
server/controllers/api/video-channel.ts
server/controllers/api/videos/index.ts
server/controllers/feeds.ts
server/helpers/custom-validators/misc.ts
server/helpers/custom-validators/search.ts [new file with mode: 0644]
server/helpers/express-utils.ts
server/initializers/constants.ts
server/middlewares/sort.ts
server/middlewares/validators/search.ts
server/models/utils.ts
server/models/video/video.ts
server/tests/api/check-params/index.ts
server/tests/api/check-params/search.ts [new file with mode: 0644]
server/tests/api/index-fast.ts
server/tests/api/search/search-videos.ts [new file with mode: 0644]
server/tests/api/videos/single-server.ts
server/tests/api/videos/video-nsfw.ts
server/tests/utils/index.ts
server/tests/utils/requests/check-api-params.ts
server/tests/utils/search/videos.ts [new file with mode: 0644]
server/tests/utils/videos/videos.ts
shared/models/index.ts
shared/models/search/index.ts [new file with mode: 0644]
shared/models/search/videos-search-query.model.ts [new file with mode: 0644]

index 8e937276cde72ac2af7499bf010a460597dcf2a1..0117fc8c65d8cc6699b3faf5f5e6469e3c52b3e6 100644 (file)
@@ -1,7 +1,7 @@
 import * as express from 'express'
 import { getFormattedObjects } from '../../helpers/utils'
 import {
-  asyncMiddleware,
+  asyncMiddleware, commonVideosFiltersValidator,
   listVideoAccountChannelsValidator,
   optionalAuthenticate,
   paginationValidator,
@@ -11,7 +11,7 @@ import {
 import { accountsNameWithHostGetValidator, accountsSortValidator, videosSortValidator } from '../../middlewares/validators'
 import { AccountModel } from '../../models/account/account'
 import { VideoModel } from '../../models/video/video'
-import { isNSFWHidden } from '../../helpers/express-utils'
+import { buildNSFWFilter } from '../../helpers/express-utils'
 import { VideoChannelModel } from '../../models/video/video-channel'
 
 const accountsRouter = express.Router()
@@ -36,6 +36,7 @@ accountsRouter.get('/:accountName/videos',
   setDefaultSort,
   setDefaultPagination,
   optionalAuthenticate,
+  commonVideosFiltersValidator,
   asyncMiddleware(listAccountVideos)
 )
 
@@ -77,7 +78,12 @@ async function listAccountVideos (req: express.Request, res: express.Response, n
     start: req.query.start,
     count: req.query.count,
     sort: req.query.sort,
-    hideNSFW: isNSFWHidden(res),
+    categoryOneOf: req.query.categoryOneOf,
+    licenceOneOf: req.query.licenceOneOf,
+    languageOneOf: req.query.languageOneOf,
+    tagsOneOf: req.query.tagsOneOf,
+    tagsAllOf: req.query.tagsAllOf,
+    nsfw: buildNSFWFilter(res, req.query.nsfw),
     withFiles: false,
     accountId: account.id
   })
index 2ff340b597df0ef566035d83918be05f499679de..f810c7452b4028348b53ff5de1c9a4303adfe54b 100644 (file)
@@ -1,9 +1,10 @@
 import * as express from 'express'
-import { isNSFWHidden } from '../../helpers/express-utils'
+import { buildNSFWFilter } from '../../helpers/express-utils'
 import { getFormattedObjects } from '../../helpers/utils'
 import { VideoModel } from '../../models/video/video'
 import {
   asyncMiddleware,
+  commonVideosFiltersValidator,
   optionalAuthenticate,
   paginationValidator,
   searchValidator,
@@ -11,6 +12,7 @@ import {
   setDefaultSearchSort,
   videosSearchSortValidator
 } from '../../middlewares'
+import { VideosSearchQuery } from '../../../shared/models/search'
 
 const searchRouter = express.Router()
 
@@ -20,6 +22,7 @@ searchRouter.get('/videos',
   videosSearchSortValidator,
   setDefaultSearchSort,
   optionalAuthenticate,
+  commonVideosFiltersValidator,
   searchValidator,
   asyncMiddleware(searchVideos)
 )
@@ -31,13 +34,10 @@ export { searchRouter }
 // ---------------------------------------------------------------------------
 
 async function searchVideos (req: express.Request, res: express.Response) {
-  const resultList = await VideoModel.searchAndPopulateAccountAndServer(
-    req.query.search as string,
-    req.query.start as number,
-    req.query.count as number,
-    req.query.sort as string,
-    isNSFWHidden(res)
-  )
+  const query: VideosSearchQuery = req.query
+
+  const options = Object.assign(query, { nsfw: buildNSFWFilter(res, query.nsfw) })
+  const resultList = await VideoModel.searchAndPopulateAccountAndServer(options)
 
   return res.json(getFormattedObjects(resultList.data, resultList.total))
 }
index 1707732ee29a8a675d0e9c6e31ab70ef93c01bdb..0488ba8f5911d1862aa3ff190940b4b90effa639 100644 (file)
@@ -3,7 +3,7 @@ import { getFormattedObjects, resetSequelizeInstance } from '../../helpers/utils
 import {
   asyncMiddleware,
   asyncRetryTransactionMiddleware,
-  authenticate,
+  authenticate, commonVideosFiltersValidator,
   optionalAuthenticate,
   paginationValidator,
   setDefaultPagination,
@@ -19,7 +19,7 @@ import { videosSortValidator } from '../../middlewares/validators'
 import { sendUpdateActor } from '../../lib/activitypub/send'
 import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared'
 import { createVideoChannel } from '../../lib/video-channel'
-import { createReqFiles, isNSFWHidden } from '../../helpers/express-utils'
+import { createReqFiles, buildNSFWFilter } from '../../helpers/express-utils'
 import { setAsyncActorKeys } from '../../lib/activitypub'
 import { AccountModel } from '../../models/account/account'
 import { CONFIG, IMAGE_MIMETYPE_EXT, sequelizeTypescript } from '../../initializers'
@@ -79,6 +79,7 @@ videoChannelRouter.get('/:id/videos',
   setDefaultSort,
   setDefaultPagination,
   optionalAuthenticate,
+  commonVideosFiltersValidator,
   asyncMiddleware(listVideoChannelVideos)
 )
 
@@ -189,7 +190,12 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon
     start: req.query.start,
     count: req.query.count,
     sort: req.query.sort,
-    hideNSFW: isNSFWHidden(res),
+    categoryOneOf: req.query.categoryOneOf,
+    licenceOneOf: req.query.licenceOneOf,
+    languageOneOf: req.query.languageOneOf,
+    tagsOneOf: req.query.tagsOneOf,
+    tagsAllOf: req.query.tagsAllOf,
+    nsfw: buildNSFWFilter(res, req.query.nsfw),
     withFiles: false,
     videoChannelId: videoChannelInstance.id
   })
index 547522123d19744119683e7dc1d8b1d40efc2648..101183eabd9ac65e2a85f944beb4639ac85d4f0d 100644 (file)
@@ -31,6 +31,7 @@ import {
   asyncMiddleware,
   asyncRetryTransactionMiddleware,
   authenticate,
+  commonVideosFiltersValidator,
   optionalAuthenticate,
   paginationValidator,
   setDefaultPagination,
@@ -49,7 +50,7 @@ import { blacklistRouter } from './blacklist'
 import { videoCommentRouter } from './comment'
 import { rateVideoRouter } from './rate'
 import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
-import { createReqFiles, isNSFWHidden } from '../../../helpers/express-utils'
+import { createReqFiles, buildNSFWFilter } from '../../../helpers/express-utils'
 import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
 import { videoCaptionsRouter } from './captions'
 
@@ -90,6 +91,7 @@ videosRouter.get('/',
   setDefaultSort,
   setDefaultPagination,
   optionalAuthenticate,
+  commonVideosFiltersValidator,
   asyncMiddleware(listVideos)
 )
 videosRouter.put('/:id',
@@ -401,8 +403,12 @@ async function listVideos (req: express.Request, res: express.Response, next: ex
     start: req.query.start,
     count: req.query.count,
     sort: req.query.sort,
-    category: req.query.category,
-    hideNSFW: isNSFWHidden(res),
+    categoryOneOf: req.query.categoryOneOf,
+    licenceOneOf: req.query.licenceOneOf,
+    languageOneOf: req.query.languageOneOf,
+    tagsOneOf: req.query.tagsOneOf,
+    tagsAllOf: req.query.tagsAllOf,
+    nsfw: buildNSFWFilter(res, req.query.nsfw),
     filter: req.query.filter as VideoFilter,
     withFiles: false
   })
index ff6b423d97cfc607cced424839dd4efb09d504f7..682f4abdaf873d56087f4b8b242b8afa0066e487 100644 (file)
@@ -8,6 +8,7 @@ import { AccountModel } from '../models/account/account'
 import { cacheRoute } from '../middlewares/cache'
 import { VideoChannelModel } from '../models/video/video-channel'
 import { VideoCommentModel } from '../models/video/video-comment'
+import { buildNSFWFilter } from '../helpers/express-utils'
 
 const feedsRouter = express.Router()
 
@@ -73,7 +74,7 @@ async function generateVideoFeed (req: express.Request, res: express.Response, n
 
   const account: AccountModel = res.locals.account
   const videoChannel: VideoChannelModel = res.locals.videoChannel
-  const hideNSFW = CONFIG.INSTANCE.DEFAULT_NSFW_POLICY === 'do_not_list'
+  const nsfw = buildNSFWFilter(res, req.query.nsfw)
 
   let name: string
   let description: string
@@ -95,7 +96,7 @@ async function generateVideoFeed (req: express.Request, res: express.Response, n
     start,
     count: FEEDS.COUNT,
     sort: req.query.sort,
-    hideNSFW,
+    nsfw,
     filter: req.query.filter,
     withFiles: true,
     accountId: account ? account.id : null,
index 455aae3674f992317161816b2fa9a0f289cae307..151fc852bae5d24c6ab99db14482a728859c9576 100644 (file)
@@ -41,6 +41,12 @@ function toValueOrNull (value: string) {
   return value
 }
 
+function toArray (value: string) {
+  if (value && isArray(value) === false) return [ value ]
+
+  return value
+}
+
 function isFileValid (
   files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[],
   mimeTypeRegex: string,
@@ -80,5 +86,6 @@ export {
   toValueOrNull,
   isBooleanValid,
   toIntOrNull,
+  toArray,
   isFileValid
 }
diff --git a/server/helpers/custom-validators/search.ts b/server/helpers/custom-validators/search.ts
new file mode 100644 (file)
index 0000000..2fde391
--- /dev/null
@@ -0,0 +1,19 @@
+import * as validator from 'validator'
+import 'express-validator'
+
+import { isArray } from './misc'
+
+function isNumberArray (value: any) {
+  return isArray(value) && value.every(v => validator.isInt('' + v))
+}
+
+function isStringArray (value: any) {
+  return isArray(value) && value.every(v => typeof v === 'string')
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  isNumberArray,
+  isStringArray
+}
index d023117a8616c8ae942a5853e74b2cd838ab962b..5bf1e1a5f513dcce46121e884f31c2bdd6fc257d 100644 (file)
@@ -5,13 +5,19 @@ import { logger } from './logger'
 import { User } from '../../shared/models/users'
 import { generateRandomString } from './utils'
 
-function isNSFWHidden (res: express.Response) {
+function buildNSFWFilter (res: express.Response, paramNSFW?: boolean) {
+  if (paramNSFW === true || paramNSFW === false) return paramNSFW
+
   if (res.locals.oauth) {
     const user: User = res.locals.oauth.token.User
-    if (user) return user.nsfwPolicy === 'do_not_list'
+    // User does not want NSFW videos
+    if (user && user.nsfwPolicy === 'do_not_list') return false
   }
 
-  return CONFIG.INSTANCE.DEFAULT_NSFW_POLICY === 'do_not_list'
+  if (CONFIG.INSTANCE.DEFAULT_NSFW_POLICY === 'do_not_list') return false
+
+  // Display all
+  return null
 }
 
 function getHostWithPort (host: string) {
@@ -70,7 +76,7 @@ function createReqFiles (
 // ---------------------------------------------------------------------------
 
 export {
-  isNSFWHidden,
+  buildNSFWFilter,
   getHostWithPort,
   badRequest,
   createReqFiles
index b966c0acb536467441e4ddf42ecf05d066c0cfc3..9f220aea5d48274dce71426654eedca16008b8f4 100644 (file)
@@ -37,7 +37,7 @@ const SORTABLE_COLUMNS = {
   FOLLOWERS: [ 'createdAt' ],
   FOLLOWING: [ 'createdAt' ],
 
-  VIDEOS_SEARCH: [ 'bestmatch', 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes' ]
+  VIDEOS_SEARCH: [ 'match', 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes' ]
 }
 
 const OAUTH_LIFETIME = {
index 6307ee1547e6359eb0c9a410525535cd941ed588..8a62c8be65a8d5e5c78c16a72ecaab69097cdfd3 100644 (file)
@@ -9,7 +9,7 @@ function setDefaultSort (req: express.Request, res: express.Response, next: expr
 }
 
 function setDefaultSearchSort (req: express.Request, res: express.Response, next: express.NextFunction) {
-  if (!req.query.sort) req.query.sort = '-bestmatch'
+  if (!req.query.sort) req.query.sort = '-match'
 
   return next()
 }
index 774845e8a38e93a55d69acf7592050d57cb2e2ff..fb2148eb3b268b38f57770482ad2cf85594e306a 100644 (file)
@@ -2,12 +2,55 @@ import * as express from 'express'
 import { areValidationErrors } from './utils'
 import { logger } from '../../helpers/logger'
 import { query } from 'express-validator/check'
+import { isNumberArray, isStringArray } from '../../helpers/custom-validators/search'
+import { isBooleanValid, isDateValid, toArray } from '../../helpers/custom-validators/misc'
 
 const searchValidator = [
   query('search').not().isEmpty().withMessage('Should have a valid search'),
 
+  query('startDate').optional().custom(isDateValid).withMessage('Should have a valid start date'),
+  query('endDate').optional().custom(isDateValid).withMessage('Should have a valid end date'),
+
+  query('durationMin').optional().isInt().withMessage('Should have a valid min duration'),
+  query('durationMax').optional().isInt().withMessage('Should have a valid max duration'),
+
+  (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    logger.debug('Checking search query', { parameters: req.query })
+
+    if (areValidationErrors(req, res)) return
+
+    return next()
+  }
+]
+
+const commonVideosFiltersValidator = [
+  query('categoryOneOf')
+    .optional()
+    .customSanitizer(toArray)
+    .custom(isNumberArray).withMessage('Should have a valid one of category array'),
+  query('licenceOneOf')
+    .optional()
+    .customSanitizer(toArray)
+    .custom(isNumberArray).withMessage('Should have a valid one of licence array'),
+  query('languageOneOf')
+    .optional()
+    .customSanitizer(toArray)
+    .custom(isStringArray).withMessage('Should have a valid one of language array'),
+  query('tagsOneOf')
+    .optional()
+    .customSanitizer(toArray)
+    .custom(isStringArray).withMessage('Should have a valid one of tags array'),
+  query('tagsAllOf')
+    .optional()
+    .customSanitizer(toArray)
+    .custom(isStringArray).withMessage('Should have a valid all of tags array'),
+  query('nsfw')
+    .optional()
+    .toBoolean()
+    .custom(isBooleanValid).withMessage('Should have a valid NSFW attribute'),
+
   (req: express.Request, res: express.Response, next: express.NextFunction) => {
-    logger.debug('Checking search parameters', { parameters: req.params })
+    logger.debug('Checking commons video filters query', { parameters: req.query })
 
     if (areValidationErrors(req, res)) return
 
@@ -18,5 +61,6 @@ const searchValidator = [
 // ---------------------------------------------------------------------------
 
 export {
+  commonVideosFiltersValidator,
   searchValidator
 }
index 49d32c24f95c11c4c9e3bbdba1cef372c68e9e7c..393f8f03631f073e3e159db0cd86121ad9d1fdb5 100644 (file)
@@ -14,7 +14,7 @@ function getSort (value: string, lastSort: string[] = [ 'id', 'ASC' ]) {
   }
 
   // Alias
-  if (field.toLowerCase() === 'bestmatch') field = Sequelize.col('similarity')
+  if (field.toLowerCase() === 'match') field = Sequelize.col('similarity')
 
   return [ [ field, direction ], lastSort ]
 }
index 15b4dda5b2fb57fee266cdab07fb422b921fc7fb..68116e3096ebe556197a8469f3592697a3d8da57 100644 (file)
@@ -93,6 +93,7 @@ import { VideoShareModel } from './video-share'
 import { VideoTagModel } from './video-tag'
 import { ScheduleVideoUpdateModel } from './schedule-video-update'
 import { VideoCaptionModel } from './video-caption'
+import { VideosSearchQuery } from '../../../shared/models/search'
 
 // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
 const indexes: Sequelize.DefineIndexesOptions[] = [
@@ -133,16 +134,22 @@ export enum ScopeNames {
   WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE'
 }
 
+type AvailableForListOptions = {
+  actorId: number,
+  filter?: VideoFilter,
+  categoryOneOf?: number[],
+  nsfw?: boolean,
+  licenceOneOf?: number[],
+  languageOneOf?: string[],
+  tagsOneOf?: string[],
+  tagsAllOf?: string[],
+  withFiles?: boolean,
+  accountId?: number,
+  videoChannelId?: number
+}
+
 @Scopes({
-  [ScopeNames.AVAILABLE_FOR_LIST]: (options: {
-    actorId: number,
-    hideNSFW: boolean,
-    filter?: VideoFilter,
-    category?: number,
-    withFiles?: boolean,
-    accountId?: number,
-    videoChannelId?: number
-  }) => {
+  [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => {
     const accountInclude = {
       attributes: [ 'id', 'name' ],
       model: AccountModel.unscoped(),
@@ -243,13 +250,55 @@ export enum ScopeNames {
       })
     }
 
-    // Hide nsfw videos?
-    if (options.hideNSFW === true) {
-      query.where['nsfw'] = false
+    // FIXME: issues with sequelize count when making a join on n:m relation, so we just make a IN()
+    if (options.tagsAllOf || options.tagsOneOf) {
+      const createTagsIn = (tags: string[]) => {
+        return tags.map(t => VideoModel.sequelize.escape(t))
+                   .join(', ')
+      }
+
+      if (options.tagsOneOf) {
+        query.where['id'][Sequelize.Op.in] = Sequelize.literal(
+          '(' +
+            'SELECT "videoId" FROM "videoTag" ' +
+            'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
+            'WHERE "tag"."name" IN (' + createTagsIn(options.tagsOneOf) + ')' +
+          ')'
+        )
+      }
+
+      if (options.tagsAllOf) {
+        query.where['id'][Sequelize.Op.in] = Sequelize.literal(
+            '(' +
+              'SELECT "videoId" FROM "videoTag" ' +
+              'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
+              'WHERE "tag"."name" IN (' + createTagsIn(options.tagsAllOf) + ')' +
+              'GROUP BY "videoTag"."videoId" HAVING COUNT(*) = ' + options.tagsAllOf.length +
+            ')'
+        )
+      }
+    }
+
+    if (options.nsfw === true || options.nsfw === false) {
+      query.where['nsfw'] = options.nsfw
+    }
+
+    if (options.categoryOneOf) {
+      query.where['category'] = {
+        [Sequelize.Op.or]: options.categoryOneOf
+      }
+    }
+
+    if (options.licenceOneOf) {
+      query.where['licence'] = {
+        [Sequelize.Op.or]: options.licenceOneOf
+      }
     }
 
-    if (options.category) {
-      query.where['category'] = options.category
+    if (options.languageOneOf) {
+      query.where['language'] = {
+        [Sequelize.Op.or]: options.languageOneOf
+      }
     }
 
     if (options.accountId) {
@@ -756,9 +805,13 @@ export class VideoModel extends Model<VideoModel> {
     start: number,
     count: number,
     sort: string,
-    hideNSFW: boolean,
+    nsfw: boolean,
     withFiles: boolean,
-    category?: number,
+    categoryOneOf?: number[],
+    licenceOneOf?: number[],
+    languageOneOf?: string[],
+    tagsOneOf?: string[],
+    tagsAllOf?: string[],
     filter?: VideoFilter,
     accountId?: number,
     videoChannelId?: number
@@ -774,13 +827,17 @@ export class VideoModel extends Model<VideoModel> {
       method: [
         ScopeNames.AVAILABLE_FOR_LIST, {
           actorId: serverActor.id,
-          hideNSFW: options.hideNSFW,
-          category: options.category,
+          nsfw: options.nsfw,
+          categoryOneOf: options.categoryOneOf,
+          licenceOneOf: options.licenceOneOf,
+          languageOneOf: options.languageOneOf,
+          tagsOneOf: options.tagsOneOf,
+          tagsAllOf: options.tagsAllOf,
           filter: options.filter,
           withFiles: options.withFiles,
           accountId: options.accountId,
           videoChannelId: options.videoChannelId
-        }
+        } as AvailableForListOptions
       ]
     }
 
@@ -794,15 +851,39 @@ export class VideoModel extends Model<VideoModel> {
       })
   }
 
-  static async searchAndPopulateAccountAndServer (value: string, start: number, count: number, sort: string, hideNSFW: boolean) {
+  static async searchAndPopulateAccountAndServer (options: VideosSearchQuery) {
+    const whereAnd = [ ]
+
+    if (options.startDate || options.endDate) {
+      const publishedAtRange = { }
+
+      if (options.startDate) publishedAtRange[Sequelize.Op.gte] = options.startDate
+      if (options.endDate) publishedAtRange[Sequelize.Op.lte] = options.endDate
+
+      whereAnd.push({ publishedAt: publishedAtRange })
+    }
+
+    if (options.durationMin || options.durationMax) {
+      const durationRange = { }
+
+      if (options.durationMin) durationRange[Sequelize.Op.gte] = options.durationMin
+      if (options.durationMax) durationRange[Sequelize.Op.lte] = options.durationMax
+
+      whereAnd.push({ duration: durationRange })
+    }
+
+    whereAnd.push(createSearchTrigramQuery('VideoModel.name', options.search))
+
     const query: IFindOptions<VideoModel> = {
       attributes: {
-        include: [ createSimilarityAttribute('VideoModel.name', value) ]
+        include: [ createSimilarityAttribute('VideoModel.name', options.search) ]
       },
-      offset: start,
-      limit: count,
-      order: getSort(sort),
-      where: createSearchTrigramQuery('VideoModel.name', value)
+      offset: options.start,
+      limit: options.count,
+      order: getSort(options.sort),
+      where: {
+        [ Sequelize.Op.and ]: whereAnd
+      }
     }
 
     const serverActor = await getServerActor()
@@ -810,8 +891,13 @@ export class VideoModel extends Model<VideoModel> {
       method: [
         ScopeNames.AVAILABLE_FOR_LIST, {
           actorId: serverActor.id,
-          hideNSFW
-        }
+          nsfw: options.nsfw,
+          categoryOneOf: options.categoryOneOf,
+          licenceOneOf: options.licenceOneOf,
+          languageOneOf: options.languageOneOf,
+          tagsOneOf: options.tagsOneOf,
+          tagsAllOf: options.tagsAllOf
+        } as AvailableForListOptions
       ]
     }
 
index c0e0302df82fa870188569c7e99f671a5ea1673e..820dde889dd40059a48328c40501ae87965a9662 100644 (file)
@@ -10,3 +10,4 @@ import './video-captions'
 import './video-channels'
 import './video-comments'
 import './videos'
+import './search'
diff --git a/server/tests/api/check-params/search.ts b/server/tests/api/check-params/search.ts
new file mode 100644 (file)
index 0000000..d35eac7
--- /dev/null
@@ -0,0 +1,122 @@
+/* tslint:disable:no-unused-expression */
+
+import 'mocha'
+
+import { flushTests, immutableAssign, killallServers, makeGetRequest, runServer, ServerInfo } from '../../utils'
+import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params'
+
+describe('Test videos API validator', function () {
+  const path = '/api/v1/search/videos/'
+  let server: ServerInfo
+
+  // ---------------------------------------------------------------
+
+  before(async function () {
+    this.timeout(30000)
+
+    await flushTests()
+
+    server = await runServer(1)
+  })
+
+  describe('When searching videos', function () {
+    const query = {
+      search: 'coucou'
+    }
+
+    it('Should fail with a bad start pagination', async function () {
+      await checkBadStartPagination(server.url, path, null, query)
+    })
+
+    it('Should fail with a bad count pagination', async function () {
+      await checkBadCountPagination(server.url, path, null, query)
+    })
+
+    it('Should fail with an incorrect sort', async function () {
+      await checkBadSortPagination(server.url, path, null, query)
+    })
+
+    it('Should success with the correct parameters', async function () {
+      await makeGetRequest({ url: server.url, path, query, statusCodeExpected: 200 })
+    })
+
+    it('Should fail with an invalid category', async function () {
+      const customQuery1 = immutableAssign(query, { categoryOneOf: [ 'aa', 'b' ] })
+      await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: 400 })
+
+      const customQuery2 = immutableAssign(query, { categoryOneOf: 'a' })
+      await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: 400 })
+    })
+
+    it('Should succeed with a valid category', async function () {
+      const customQuery1 = immutableAssign(query, { categoryOneOf: [ 1, 7 ] })
+      await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: 200 })
+
+      const customQuery2 = immutableAssign(query, { categoryOneOf: 1 })
+      await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: 200 })
+    })
+
+    it('Should fail with an invalid licence', async function () {
+      const customQuery1 = immutableAssign(query, { licenceOneOf: [ 'aa', 'b' ] })
+      await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: 400 })
+
+      const customQuery2 = immutableAssign(query, { licenceOneOf: 'a' })
+      await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: 400 })
+    })
+
+    it('Should succeed with a valid licence', async function () {
+      const customQuery1 = immutableAssign(query, { licenceOneOf: [ 1, 2 ] })
+      await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: 200 })
+
+      const customQuery2 = immutableAssign(query, { licenceOneOf: 1 })
+      await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: 200 })
+    })
+
+    it('Should succeed with a valid language', async function () {
+      const customQuery1 = immutableAssign(query, { languageOneOf: [ 'fr', 'en' ] })
+      await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: 200 })
+
+      const customQuery2 = immutableAssign(query, { languageOneOf: 'fr' })
+      await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: 200 })
+    })
+
+    it('Should succeed with valid tags', async function () {
+      const customQuery1 = immutableAssign(query, { tagsOneOf: [ 'tag1', 'tag2' ] })
+      await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: 200 })
+
+      const customQuery2 = immutableAssign(query, { tagsOneOf: 'tag1' })
+      await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: 200 })
+
+      const customQuery3 = immutableAssign(query, { tagsAllOf: [ 'tag1', 'tag2' ] })
+      await makeGetRequest({ url: server.url, path, query: customQuery3, statusCodeExpected: 200 })
+
+      const customQuery4 = immutableAssign(query, { tagsAllOf: 'tag1' })
+      await makeGetRequest({ url: server.url, path, query: customQuery4, statusCodeExpected: 200 })
+    })
+
+    it('Should fail with invalid durations', async function () {
+      const customQuery1 = immutableAssign(query, { durationMin: 'hello' })
+      await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: 400 })
+
+      const customQuery2 = immutableAssign(query, { durationMax: 'hello' })
+      await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: 400 })
+    })
+
+    it('Should fail with invalid dates', async function () {
+      const customQuery1 = immutableAssign(query, { startDate: 'hello' })
+      await makeGetRequest({ url: server.url, path, query: customQuery1, statusCodeExpected: 400 })
+
+      const customQuery2 = immutableAssign(query, { endDate: 'hello' })
+      await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: 400 })
+    })
+  })
+
+  after(async function () {
+    killallServers([ server ])
+
+    // Keep the logs if the test failed
+    if (this['ok']) {
+      await flushTests()
+    }
+  })
+})
index d530dfc06640df40ed9979e4fc969fa24be8e713..531a09b82418769d7d320a6da1642dfc90840451 100644 (file)
@@ -14,3 +14,4 @@ import './videos/services'
 import './server/email'
 import './server/config'
 import './server/reverse-proxy'
+import './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 (file)
index 0000000..7fc133b
--- /dev/null
@@ -0,0 +1,299 @@
+/* tslint:disable:no-unused-expression */
+
+import * as chai from 'chai'
+import 'mocha'
+import {
+  advancedVideosSearch,
+  flushTests,
+  killallServers,
+  runServer,
+  searchVideo,
+  ServerInfo,
+  setAccessTokensToServers,
+  uploadVideo,
+  wait,
+  immutableAssign
+} from '../../utils'
+
+const expect = chai.expect
+
+describe('Test a videos search', function () {
+  let server: ServerInfo = null
+  let startDate: string
+
+  before(async function () {
+    this.timeout(30000)
+
+    await flushTests()
+
+    server = await runServer(1)
+
+    await setAccessTokensToServers([ server ])
+
+    {
+      const attributes1 = {
+        name: '1111 2222 3333',
+        fixture: '60fps_720p_small.mp4', // 2 seconds
+        category: 1,
+        licence: 1,
+        nsfw: false,
+        language: 'fr'
+      }
+      await uploadVideo(server.url, server.accessToken, attributes1)
+
+      const attributes2 = immutableAssign(attributes1, { name: attributes1.name + ' - 2', fixture: 'video_short.mp4' })
+      await uploadVideo(server.url, server.accessToken, attributes2)
+
+      const attributes3 = immutableAssign(attributes1, { name: attributes1.name + ' - 3', language: 'en' })
+      await uploadVideo(server.url, server.accessToken, attributes3)
+
+      const attributes4 = immutableAssign(attributes1, { name: attributes1.name + ' - 4', language: 'pl', nsfw: true })
+      await uploadVideo(server.url, server.accessToken, attributes4)
+
+      await wait(1000)
+
+      startDate = new Date().toISOString()
+
+      const attributes5 = immutableAssign(attributes1, { name: attributes1.name + ' - 5', licence: 2 })
+      await uploadVideo(server.url, server.accessToken, attributes5)
+
+      const attributes6 = immutableAssign(attributes1, { name: attributes1.name + ' - 6', tags: [ 't1', 't2 '] })
+      await uploadVideo(server.url, server.accessToken, attributes6)
+
+      const attributes7 = immutableAssign(attributes1, { name: attributes1.name + ' - 7' })
+      await uploadVideo(server.url, server.accessToken, attributes7)
+
+      const attributes8 = immutableAssign(attributes1, { name: attributes1.name + ' - 8', licence: 4 })
+      await uploadVideo(server.url, server.accessToken, attributes8)
+    }
+
+    {
+      const attributes = {
+        name: '3333 4444 5555',
+        fixture: 'video_short.mp4',
+        category: 2,
+        licence: 2,
+        language: 'en'
+      }
+      await uploadVideo(server.url, server.accessToken, attributes)
+
+      await uploadVideo(server.url, server.accessToken, immutableAssign(attributes, { name: attributes.name + ' duplicate' }))
+    }
+
+    {
+      const attributes = {
+        name: '6666 7777 8888',
+        fixture: 'video_short.mp4',
+        category: 3,
+        licence: 3,
+        language: 'pl'
+      }
+      await uploadVideo(server.url, server.accessToken, attributes)
+    }
+
+    {
+      const attributes1 = {
+        name: '9999',
+        tags: [ 'aaaa', 'bbbb', 'cccc' ],
+        category: 1
+      }
+      await uploadVideo(server.url, server.accessToken, attributes1)
+      await uploadVideo(server.url, server.accessToken, immutableAssign(attributes1, { category: 2 }))
+
+      await uploadVideo(server.url, server.accessToken, immutableAssign(attributes1, { tags: [ 'cccc', 'dddd' ] }))
+      await uploadVideo(server.url, server.accessToken, immutableAssign(attributes1, { tags: [ 'eeee', 'ffff' ] }))
+    }
+  })
+
+  it('Should make a simple search and not have results', async function () {
+    const res = await searchVideo(server.url, 'abc')
+
+    expect(res.body.total).to.equal(0)
+    expect(res.body.data).to.have.lengthOf(0)
+  })
+
+  it('Should make a simple search and have results', async function () {
+    const res = await searchVideo(server.url, '4444 5555 duplicate')
+
+    expect(res.body.total).to.equal(2)
+
+    const videos = res.body.data
+    expect(videos).to.have.lengthOf(2)
+
+    // bestmatch
+    expect(videos[0].name).to.equal('3333 4444 5555 duplicate')
+    expect(videos[1].name).to.equal('3333 4444 5555')
+  })
+
+  it('Should search by tags (one of)', async function () {
+    const query = {
+      search: '9999',
+      categoryOneOf: [ 1 ],
+      tagsOneOf: [ 'aaaa', 'ffff' ]
+    }
+    const res1 = await advancedVideosSearch(server.url, query)
+    expect(res1.body.total).to.equal(2)
+
+    const res2 = await advancedVideosSearch(server.url, immutableAssign(query, { tagsOneOf: [ 'blabla' ] }))
+    expect(res2.body.total).to.equal(0)
+  })
+
+  it('Should search by tags (all of)', async function () {
+    const query = {
+      search: '9999',
+      categoryOneOf: [ 1 ],
+      tagsAllOf: [ 'cccc' ]
+    }
+    const res1 = await advancedVideosSearch(server.url, query)
+    expect(res1.body.total).to.equal(2)
+
+    const res2 = await advancedVideosSearch(server.url, immutableAssign(query, { tagsAllOf: [ 'blabla' ] }))
+    expect(res2.body.total).to.equal(0)
+
+    const res3 = await advancedVideosSearch(server.url, immutableAssign(query, { tagsAllOf: [ 'bbbb', 'cccc' ] }))
+    expect(res3.body.total).to.equal(1)
+  })
+
+  it('Should search by category', async function () {
+    const query = {
+      search: '6666',
+      categoryOneOf: [ 3 ]
+    }
+    const res1 = await advancedVideosSearch(server.url, query)
+    expect(res1.body.total).to.equal(1)
+    expect(res1.body.data[0].name).to.equal('6666 7777 8888')
+
+    const res2 = await advancedVideosSearch(server.url, immutableAssign(query, { categoryOneOf: [ 2 ] }))
+    expect(res2.body.total).to.equal(0)
+  })
+
+  it('Should search by licence', async function () {
+    const query = {
+      search: '4444 5555',
+      licenceOneOf: [ 2 ]
+    }
+    const res1 = await advancedVideosSearch(server.url, query)
+    expect(res1.body.total).to.equal(2)
+    expect(res1.body.data[0].name).to.equal('3333 4444 5555')
+    expect(res1.body.data[1].name).to.equal('3333 4444 5555 duplicate')
+
+    const res2 = await advancedVideosSearch(server.url, immutableAssign(query, { licenceOneOf: [ 3 ] }))
+    expect(res2.body.total).to.equal(0)
+  })
+
+  it('Should search by languages', async function () {
+    const query = {
+      search: '1111 2222 3333',
+      languageOneOf: [ 'pl', 'en' ]
+    }
+    const res1 = await advancedVideosSearch(server.url, query)
+    expect(res1.body.total).to.equal(2)
+    expect(res1.body.data[0].name).to.equal('1111 2222 3333 - 3')
+    expect(res1.body.data[1].name).to.equal('1111 2222 3333 - 4')
+
+    const res2 = await advancedVideosSearch(server.url, immutableAssign(query, { languageOneOf: [ 'eo' ] }))
+    expect(res2.body.total).to.equal(0)
+  })
+
+  it('Should search by start date', async function () {
+    const query = {
+      search: '1111 2222 3333',
+      startDate
+    }
+
+    const res = await advancedVideosSearch(server.url, query)
+    expect(res.body.total).to.equal(4)
+
+    const videos = res.body.data
+    expect(videos[0].name).to.equal('1111 2222 3333 - 5')
+    expect(videos[1].name).to.equal('1111 2222 3333 - 6')
+    expect(videos[2].name).to.equal('1111 2222 3333 - 7')
+    expect(videos[3].name).to.equal('1111 2222 3333 - 8')
+  })
+
+  it('Should make an advanced search', async function () {
+    const query = {
+      search: '1111 2222 3333',
+      languageOneOf: [ 'pl', 'fr' ],
+      durationMax: 4,
+      nsfw: false,
+      licenceOneOf: [ 1, 4 ]
+    }
+
+    const res = await advancedVideosSearch(server.url, query)
+    expect(res.body.total).to.equal(4)
+
+    const videos = res.body.data
+    expect(videos[0].name).to.equal('1111 2222 3333')
+    expect(videos[1].name).to.equal('1111 2222 3333 - 6')
+    expect(videos[2].name).to.equal('1111 2222 3333 - 7')
+    expect(videos[3].name).to.equal('1111 2222 3333 - 8')
+  })
+
+  it('Should make an advanced search and sort results', async function () {
+    const query = {
+      search: '1111 2222 3333',
+      languageOneOf: [ 'pl', 'fr' ],
+      durationMax: 4,
+      nsfw: false,
+      licenceOneOf: [ 1, 4 ],
+      sort: '-name'
+    }
+
+    const res = await advancedVideosSearch(server.url, query)
+    expect(res.body.total).to.equal(4)
+
+    const videos = res.body.data
+    expect(videos[0].name).to.equal('1111 2222 3333 - 8')
+    expect(videos[1].name).to.equal('1111 2222 3333 - 7')
+    expect(videos[2].name).to.equal('1111 2222 3333 - 6')
+    expect(videos[3].name).to.equal('1111 2222 3333')
+  })
+
+  it('Should make an advanced search and only show the first result', async function () {
+    const query = {
+      search: '1111 2222 3333',
+      languageOneOf: [ 'pl', 'fr' ],
+      durationMax: 4,
+      nsfw: false,
+      licenceOneOf: [ 1, 4 ],
+      sort: '-name',
+      start: 0,
+      count: 1
+    }
+
+    const res = await advancedVideosSearch(server.url, query)
+    expect(res.body.total).to.equal(4)
+
+    const videos = res.body.data
+    expect(videos[0].name).to.equal('1111 2222 3333 - 8')
+  })
+
+  it('Should make an advanced search and only show the last result', async function () {
+    const query = {
+      search: '1111 2222 3333',
+      languageOneOf: [ 'pl', 'fr' ],
+      durationMax: 4,
+      nsfw: false,
+      licenceOneOf: [ 1, 4 ],
+      sort: '-name',
+      start: 3,
+      count: 1
+    }
+
+    const res = await advancedVideosSearch(server.url, query)
+    expect(res.body.total).to.equal(4)
+
+    const videos = res.body.data
+    expect(videos[0].name).to.equal('1111 2222 3333')
+  })
+
+  after(async function () {
+    killallServers([ server ])
+
+    // Keep the logs if the test failed
+    if (this['ok']) {
+      await flushTests()
+    }
+  })
+})
index d8af94e8f9302fe98bf6442b9af2edad511c3073..ba4920d1b36fef518c1c3c0b0ef5063df06c5971 100644 (file)
@@ -16,13 +16,11 @@ import {
   getVideosList,
   getVideosListPagination,
   getVideosListSort,
+  getVideosWithFilters,
   killallServers,
   rateVideo,
   removeVideo,
   runServer,
-  searchVideo,
-  searchVideoWithPagination,
-  searchVideoWithSort,
   ServerInfo,
   setAccessTokensToServers,
   testImage,
@@ -218,72 +216,6 @@ describe('Test a single server', function () {
     expect(video.views).to.equal(3)
   })
 
-  it('Should search the video by name', async function () {
-    const res = await searchVideo(server.url, 'my')
-
-    expect(res.body.total).to.equal(1)
-    expect(res.body.data).to.be.an('array')
-    expect(res.body.data.length).to.equal(1)
-
-    const video = res.body.data[0]
-    await completeVideoCheck(server.url, video, getCheckAttributes)
-  })
-
-  // Not implemented yet
-  // it('Should search the video by tag', async function () {
-  //   const res = await searchVideo(server.url, 'tag1')
-  //
-  //   expect(res.body.total).to.equal(1)
-  //   expect(res.body.data).to.be.an('array')
-  //   expect(res.body.data.length).to.equal(1)
-  //
-  //   const video = res.body.data[0]
-  //   expect(video.name).to.equal('my super name')
-  //   expect(video.category).to.equal(2)
-  //   expect(video.categoryLabel).to.equal('Films')
-  //   expect(video.licence).to.equal(6)
-  //   expect(video.licenceLabel).to.equal('Attribution - Non Commercial - No Derivatives')
-  //   expect(video.language).to.equal('zh')
-  //   expect(video.languageLabel).to.equal('Chinese')
-  //   expect(video.nsfw).to.be.ok
-  //   expect(video.description).to.equal('my super description')
-  //   expect(video.account.name).to.equal('root')
-  //   expect(video.account.host).to.equal('localhost:9001')
-  //   expect(video.isLocal).to.be.true
-  //   expect(video.tags).to.deep.equal([ 'tag1', 'tag2', 'tag3' ])
-  //   expect(dateIsValid(video.createdAt)).to.be.true
-  //   expect(dateIsValid(video.updatedAt)).to.be.true
-  //
-  //   const test = await testVideoImage(server.url, 'video_short.webm', video.thumbnailPath)
-  //   expect(test).to.equal(true)
-  // })
-
-  it('Should not find a search by name', async function () {
-    const res = await searchVideo(server.url, 'hello')
-
-    expect(res.body.total).to.equal(0)
-    expect(res.body.data).to.be.an('array')
-    expect(res.body.data.length).to.equal(0)
-  })
-
-  // Not implemented yet
-  // it('Should not find a search by author', async function () {
-  //   const res = await searchVideo(server.url, 'hello')
-  //
-  //   expect(res.body.total).to.equal(0)
-  //   expect(res.body.data).to.be.an('array')
-  //   expect(res.body.data.length).to.equal(0)
-  // })
-  //
-  // Not implemented yet
-  // it('Should not find a search by tag', async function () {
-  //   const res = await searchVideo(server.url, 'hello')
-  //
-  //   expect(res.body.total).to.equal(0)
-  //   expect(res.body.data).to.be.an('array')
-  //   expect(res.body.data.length).to.equal(0)
-  // })
-
   it('Should remove the video', async function () {
     await removeVideo(server.url, server.accessToken, videoId)
 
@@ -386,65 +318,6 @@ describe('Test a single server', function () {
     expect(videos[0].name).to.equal(videosListBase[5].name)
   })
 
-  it('Should search the first video', async function () {
-    const res = await searchVideoWithPagination(server.url, 'webm', 0, 1, 'name')
-
-    const videos = res.body.data
-    expect(res.body.total).to.equal(4)
-    expect(videos.length).to.equal(1)
-    expect(videos[0].name).to.equal('video_short1.webm name')
-  })
-
-  it('Should search the last two videos', async function () {
-    const res = await searchVideoWithPagination(server.url, 'webm', 2, 2, 'name')
-
-    const videos = res.body.data
-    expect(res.body.total).to.equal(4)
-    expect(videos.length).to.equal(2)
-    expect(videos[0].name).to.equal('video_short3.webm name')
-    expect(videos[1].name).to.equal('video_short.webm name')
-  })
-
-  it('Should search all the webm videos', async function () {
-    const res = await searchVideoWithPagination(server.url, 'webm', 0, 15)
-
-    const videos = res.body.data
-    expect(res.body.total).to.equal(4)
-    expect(videos.length).to.equal(4)
-  })
-
-  // Not implemented yet
-  // it('Should search all the root author videos', async function () {
-  //   const res = await searchVideoWithPagination(server.url, 'root', 0, 15)
-  //
-  //   const videos = res.body.data
-  //   expect(res.body.total).to.equal(6)
-  //   expect(videos.length).to.equal(6)
-  // })
-
-  // Not implemented yet
-  // it('Should search all the 9001 port videos', async function () {
-  // const res = await   videosUtils.searchVideoWithPagination(server.url, '9001', 'host', 0, 15)
-
-  //     const videos = res.body.data
-  //     expect(res.body.total).to.equal(6)
-  //     expect(videos.length).to.equal(6)
-
-  //     done()
-  //   })
-  // })
-
-  // it('Should search all the localhost videos', async function () {
-  // const res = await   videosUtils.searchVideoWithPagination(server.url, 'localhost', 'host', 0, 15)
-
-  //     const videos = res.body.data
-  //     expect(res.body.total).to.equal(6)
-  //     expect(videos.length).to.equal(6)
-
-  //     done()
-  //   })
-  // })
-
   it('Should list and sort by name in descending order', async function () {
     const res = await getVideosListSort(server.url, '-name')
 
@@ -457,21 +330,8 @@ describe('Test a single server', function () {
     expect(videos[3].name).to.equal('video_short3.webm name')
     expect(videos[4].name).to.equal('video_short2.webm name')
     expect(videos[5].name).to.equal('video_short1.webm name')
-  })
-
-  it('Should search and sort by name in ascending order', async function () {
-    const res = await searchVideoWithSort(server.url, 'webm', 'name')
 
-    const videos = res.body.data
-    expect(res.body.total).to.equal(4)
-    expect(videos.length).to.equal(4)
-
-    expect(videos[0].name).to.equal('video_short1.webm name')
-    expect(videos[1].name).to.equal('video_short2.webm name')
-    expect(videos[2].name).to.equal('video_short3.webm name')
-    expect(videos[3].name).to.equal('video_short.webm name')
-
-    videoId = videos[2].id
+    videoId = videos[3].uuid
   })
 
   it('Should update a video', async function () {
@@ -488,6 +348,15 @@ describe('Test a single server', function () {
     await updateVideo(server.url, server.accessToken, videoId, attributes)
   })
 
+  it('Should filter by tags and category', async function () {
+    const res1 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: 4 })
+    expect(res1.body.total).to.equal(1)
+    expect(res1.body.data[0].name).to.equal('my super video updated')
+
+    const res2 = await getVideosWithFilters(server.url, { tagsAllOf: [ 'tagup1', 'tagup2' ], categoryOneOf: 3 })
+    expect(res2.body.total).to.equal(0)
+  })
+
   it('Should have the video updated', async function () {
     this.timeout(60000)
 
index 6af0ca8af72472ee8318c93ea1d040b599371fa0..38bdaa54e312c67140d3445c87c5836057d6b9f0 100644 (file)
@@ -30,7 +30,7 @@ describe('Test video NSFW policy', function () {
   let userAccessToken: string
   let customConfig: CustomConfig
 
-  function getVideosFunctions (token?: string) {
+  function getVideosFunctions (token?: string, query = {}) {
     return getMyUserInformation(server.url, server.accessToken)
       .then(res => {
         const user: User = res.body
@@ -39,10 +39,10 @@ describe('Test video NSFW policy', function () {
 
         if (token) {
           return Promise.all([
-            getVideosListWithToken(server.url, token),
-            searchVideoWithToken(server.url, 'n', token),
-            getAccountVideos(server.url, token, accountName, 0, 5),
-            getVideoChannelVideos(server.url, token, videoChannelUUID, 0, 5)
+            getVideosListWithToken(server.url, token, query),
+            searchVideoWithToken(server.url, 'n', token, query),
+            getAccountVideos(server.url, token, accountName, 0, 5, undefined, query),
+            getVideoChannelVideos(server.url, token, videoChannelUUID, 0, 5, undefined, query)
           ])
         }
 
@@ -200,6 +200,26 @@ describe('Test video NSFW policy', function () {
       expect(videos[ 0 ].name).to.equal('normal')
       expect(videos[ 1 ].name).to.equal('nsfw')
     })
+
+    it('Should display NSFW videos when the nsfw param === true', async function () {
+      for (const res of await getVideosFunctions(server.accessToken, { nsfw: true })) {
+        expect(res.body.total).to.equal(1)
+
+        const videos = res.body.data
+        expect(videos).to.have.lengthOf(1)
+        expect(videos[ 0 ].name).to.equal('nsfw')
+      }
+    })
+
+    it('Should hide NSFW videos when the nsfw param === true', async function () {
+      for (const res of await getVideosFunctions(server.accessToken, { nsfw: false })) {
+        expect(res.body.total).to.equal(1)
+
+        const videos = res.body.data
+        expect(videos).to.have.lengthOf(1)
+        expect(videos[ 0 ].name).to.equal('normal')
+      }
+    })
   })
 
   after(async function () {
index 5b560ca39d793cb92ff16efb29d486d80eea6f4e..391db18cf09f21f065b996299e68c14c114d8d0e 100644 (file)
@@ -14,3 +14,4 @@ export * from './videos/video-blacklist'
 export * from './videos/video-channels'
 export * from './videos/videos'
 export * from './feeds/feeds'
+export * from './search/videos'
index 7550eb3d8e10bd463f05bae0e78198dc2bdb9013..edb47e0e96dd092ff20e6a18e76af50698dad158 100644 (file)
@@ -1,31 +1,32 @@
 import { makeGetRequest } from './requests'
+import { immutableAssign } from '..'
 
-function checkBadStartPagination (url: string, path: string, token?: string) {
+function checkBadStartPagination (url: string, path: string, token?: string, query = {}) {
   return makeGetRequest({
     url,
     path,
     token,
-    query: { start: 'hello' },
+    query: immutableAssign(query, { start: 'hello' }),
     statusCodeExpected: 400
   })
 }
 
-function checkBadCountPagination (url: string, path: string, token?: string) {
+function checkBadCountPagination (url: string, path: string, token?: string, query = {}) {
   return makeGetRequest({
     url,
     path,
     token,
-    query: { count: 'hello' },
+    query: immutableAssign(query, { count: 'hello' }),
     statusCodeExpected: 400
   })
 }
 
-function checkBadSortPagination (url: string, path: string, token?: string) {
+function checkBadSortPagination (url: string, path: string, token?: string, query = {}) {
   return makeGetRequest({
     url,
     path,
     token,
-    query: { sort: 'hello' },
+    query: immutableAssign(query, { sort: 'hello' }),
     statusCodeExpected: 400
   })
 }
diff --git a/server/tests/utils/search/videos.ts b/server/tests/utils/search/videos.ts
new file mode 100644 (file)
index 0000000..3a0c10e
--- /dev/null
@@ -0,0 +1,77 @@
+/* tslint:disable:no-unused-expression */
+
+import * as request from 'supertest'
+import { VideosSearchQuery } from '../../../../shared/models/search'
+import { immutableAssign } from '..'
+
+function searchVideo (url: string, search: string) {
+  const path = '/api/v1/search/videos'
+  const req = request(url)
+    .get(path)
+    .query({ sort: '-publishedAt', search })
+    .set('Accept', 'application/json')
+
+  return req.expect(200)
+            .expect('Content-Type', /json/)
+}
+
+function searchVideoWithToken (url: string, search: string, token: string, query: { nsfw?: boolean } = {}) {
+  const path = '/api/v1/search/videos'
+  const req = request(url)
+    .get(path)
+    .set('Authorization', 'Bearer ' + token)
+    .query(immutableAssign(query, { sort: '-publishedAt', search }))
+    .set('Accept', 'application/json')
+
+  return req.expect(200)
+            .expect('Content-Type', /json/)
+}
+
+function searchVideoWithPagination (url: string, search: string, start: number, count: number, sort?: string) {
+  const path = '/api/v1/search/videos'
+
+  const req = request(url)
+    .get(path)
+    .query({ start })
+    .query({ search })
+    .query({ count })
+
+  if (sort) req.query({ sort })
+
+  return req.set('Accept', 'application/json')
+            .expect(200)
+            .expect('Content-Type', /json/)
+}
+
+function searchVideoWithSort (url: string, search: string, sort: string) {
+  const path = '/api/v1/search/videos'
+
+  return request(url)
+    .get(path)
+    .query({ search })
+    .query({ sort })
+    .set('Accept', 'application/json')
+    .expect(200)
+    .expect('Content-Type', /json/)
+}
+
+function advancedVideosSearch (url: string, options: VideosSearchQuery) {
+  const path = '/api/v1/search/videos'
+
+  return request(url)
+    .get(path)
+    .query(options)
+    .set('Accept', 'application/json')
+    .expect(200)
+    .expect('Content-Type', /json/)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  searchVideo,
+  advancedVideosSearch,
+  searchVideoWithToken,
+  searchVideoWithPagination,
+  searchVideoWithSort
+}
index a42d0f0431642041bd7fdc364ea26ba1c52d89fd..8c49eb02bf4735af1f6b00b364cc0945d241271b 100644 (file)
@@ -7,7 +7,7 @@ import { extname, join } from 'path'
 import * as request from 'supertest'
 import {
   buildAbsoluteFixturePath,
-  getMyUserInformation,
+  getMyUserInformation, immutableAssign,
   makeGetRequest,
   makePutBodyRequest,
   makeUploadRequest,
@@ -133,13 +133,13 @@ function getVideosList (url: string) {
           .expect('Content-Type', /json/)
 }
 
-function getVideosListWithToken (url: string, token: string) {
+function getVideosListWithToken (url: string, token: string, query: { nsfw?: boolean } = {}) {
   const path = '/api/v1/videos'
 
   return request(url)
     .get(path)
     .set('Authorization', 'Bearer ' + token)
-    .query({ sort: 'name' })
+    .query(immutableAssign(query, { sort: 'name' }))
     .set('Accept', 'application/json')
     .expect(200)
     .expect('Content-Type', /json/)
@@ -172,17 +172,25 @@ function getMyVideos (url: string, accessToken: string, start: number, count: nu
     .expect('Content-Type', /json/)
 }
 
-function getAccountVideos (url: string, accessToken: string, accountName: string, start: number, count: number, sort?: string) {
+function getAccountVideos (
+  url: string,
+  accessToken: string,
+  accountName: string,
+  start: number,
+  count: number,
+  sort?: string,
+  query: { nsfw?: boolean } = {}
+) {
   const path = '/api/v1/accounts/' + accountName + '/videos'
 
   return makeGetRequest({
     url,
     path,
-    query: {
+    query: immutableAssign(query, {
       start,
       count,
       sort
-    },
+    }),
     token: accessToken,
     statusCodeExpected: 200
   })
@@ -194,18 +202,19 @@ function getVideoChannelVideos (
   videoChannelId: number | string,
   start: number,
   count: number,
-  sort?: string
+  sort?: string,
+  query: { nsfw?: boolean } = {}
 ) {
   const path = '/api/v1/video-channels/' + videoChannelId + '/videos'
 
   return makeGetRequest({
     url,
     path,
-    query: {
+    query: immutableAssign(query, {
       start,
       count,
       sort
-    },
+    }),
     token: accessToken,
     statusCodeExpected: 200
   })
@@ -237,65 +246,25 @@ function getVideosListSort (url: string, sort: string) {
           .expect('Content-Type', /json/)
 }
 
-function removeVideo (url: string, token: string, id: number | string, expectedStatus = 204) {
+function getVideosWithFilters (url: string, query: { tagsAllOf: string[], categoryOneOf: number[] | number }) {
   const path = '/api/v1/videos'
 
   return request(url)
-          .delete(path + '/' + id)
-          .set('Accept', 'application/json')
-          .set('Authorization', 'Bearer ' + token)
-          .expect(expectedStatus)
-}
-
-function searchVideo (url: string, search: string) {
-  const path = '/api/v1/search/videos'
-  const req = request(url)
     .get(path)
-    .query({ search })
+    .query(query)
     .set('Accept', 'application/json')
-
-  return req.expect(200)
+    .expect(200)
     .expect('Content-Type', /json/)
 }
 
-function searchVideoWithToken (url: string, search: string, token: string) {
+function removeVideo (url: string, token: string, id: number | string, expectedStatus = 204) {
   const path = '/api/v1/videos'
-  const req = request(url)
-    .get(path + '/search')
-    .set('Authorization', 'Bearer ' + token)
-    .query({ search })
-    .set('Accept', 'application/json')
-
-  return req.expect(200)
-            .expect('Content-Type', /json/)
-}
-
-function searchVideoWithPagination (url: string, search: string, start: number, count: number, sort?: string) {
-  const path = '/api/v1/search/videos'
-
-  const req = request(url)
-                .get(path)
-                .query({ start })
-                .query({ search })
-                .query({ count })
-
-  if (sort) req.query({ sort })
-
-  return req.set('Accept', 'application/json')
-            .expect(200)
-            .expect('Content-Type', /json/)
-}
-
-function searchVideoWithSort (url: string, search: string, sort: string) {
-  const path = '/api/v1/search/videos'
 
   return request(url)
-          .get(path)
-          .query({ search })
-          .query({ sort })
+          .delete(path + '/' + id)
           .set('Accept', 'application/json')
-          .expect(200)
-          .expect('Content-Type', /json/)
+          .set('Authorization', 'Bearer ' + token)
+          .expect(expectedStatus)
 }
 
 async function checkVideoFilesWereRemoved (videoUUID: string, serverNumber: number) {
@@ -581,18 +550,15 @@ export {
   getMyVideos,
   getAccountVideos,
   getVideoChannelVideos,
-  searchVideoWithToken,
   getVideo,
   getVideoWithToken,
   getVideosList,
   getVideosListPagination,
   getVideosListSort,
   removeVideo,
-  searchVideo,
-  searchVideoWithPagination,
-  searchVideoWithSort,
   getVideosListWithToken,
   uploadVideo,
+  getVideosWithFilters,
   updateVideo,
   rateVideo,
   viewVideo,
index c8ce71f1727d7457a054336a428618d21f4a55ca..1db00c295b8ee26369215e7f278f0c4aaf656e16 100644 (file)
@@ -4,6 +4,7 @@ export * from './users'
 export * from './videos'
 export * from './feeds'
 export * from './i18n'
+export * from './search'
 export * from './server/job.model'
 export * from './oauth-client-local.model'
 export * from './result-list.model'
diff --git a/shared/models/search/index.ts b/shared/models/search/index.ts
new file mode 100644 (file)
index 0000000..288ee41
--- /dev/null
@@ -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 (file)
index 0000000..bb23bd6
--- /dev/null
@@ -0,0 +1,24 @@
+export interface VideosSearchQuery {
+  search: string
+
+  start?: number
+  count?: number
+  sort?: string
+
+  startDate?: string // ISO 8601
+  endDate?: string // ISO 8601
+
+  nsfw?: boolean
+
+  categoryOneOf?: number[]
+
+  licenceOneOf?: number[]
+
+  languageOneOf?: string[]
+
+  tagsOneOf?: string[]
+  tagsAllOf?: string[]
+
+  durationMin?: number // seconds
+  durationMax?: number // seconds
+}