diff options
author | Chocobozzz <me@florianbigard.com> | 2021-07-28 16:40:21 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2021-07-28 16:40:21 +0200 |
commit | fbd67e7f386504e50f2504cb6386700a58906f16 (patch) | |
tree | 1a7143aaea76ce4e195fb9d6214a0cd769c556ea | |
parent | 164c8d46cf5c948a28b4ac0e596fad9b83b2c229 (diff) | |
download | PeerTube-fbd67e7f386504e50f2504cb6386700a58906f16.tar.gz PeerTube-fbd67e7f386504e50f2504cb6386700a58906f16.tar.zst PeerTube-fbd67e7f386504e50f2504cb6386700a58906f16.zip |
Add ability to search by uuids/actor names
-rw-r--r-- | server/controllers/api/search/search-video-channels.ts | 7 | ||||
-rw-r--r-- | server/controllers/api/search/search-video-playlists.ts | 3 | ||||
-rw-r--r-- | server/helpers/custom-validators/misc.ts | 10 | ||||
-rw-r--r-- | server/middlewares/validators/search.ts | 34 | ||||
-rw-r--r-- | server/models/video/sql/videos-id-list-query-builder.ts | 10 | ||||
-rw-r--r-- | server/models/video/video-channel.ts | 76 | ||||
-rw-r--r-- | server/models/video/video-playlist.ts | 16 | ||||
-rw-r--r-- | server/models/video/video.ts | 3 | ||||
-rw-r--r-- | server/tests/api/check-params/search.ts | 15 | ||||
-rw-r--r-- | server/tests/api/search/search-channels.ts | 24 | ||||
-rw-r--r-- | server/tests/api/search/search-playlists.ts | 28 | ||||
-rw-r--r-- | server/tests/api/search/search-videos.ts | 28 | ||||
-rw-r--r-- | shared/models/search/video-channels-search-query.model.ts | 3 | ||||
-rw-r--r-- | shared/models/search/video-playlists-search-query.model.ts | 3 | ||||
-rw-r--r-- | shared/models/search/videos-search-query.model.ts | 3 |
15 files changed, 215 insertions, 48 deletions
diff --git a/server/controllers/api/search/search-video-channels.ts b/server/controllers/api/search/search-video-channels.ts index be0b6b9a2..9fc2d53a5 100644 --- a/server/controllers/api/search/search-video-channels.ts +++ b/server/controllers/api/search/search-video-channels.ts | |||
@@ -46,7 +46,7 @@ export { searchChannelsRouter } | |||
46 | 46 | ||
47 | function searchVideoChannels (req: express.Request, res: express.Response) { | 47 | function searchVideoChannels (req: express.Request, res: express.Response) { |
48 | const query: VideoChannelsSearchQuery = req.query | 48 | const query: VideoChannelsSearchQuery = req.query |
49 | const search = query.search | 49 | let search = query.search || '' |
50 | 50 | ||
51 | const parts = search.split('@') | 51 | const parts = search.split('@') |
52 | 52 | ||
@@ -57,7 +57,7 @@ function searchVideoChannels (req: express.Request, res: express.Response) { | |||
57 | if (isURISearch(search) || isWebfingerSearch) return searchVideoChannelURI(search, isWebfingerSearch, res) | 57 | if (isURISearch(search) || isWebfingerSearch) return searchVideoChannelURI(search, isWebfingerSearch, res) |
58 | 58 | ||
59 | // @username -> username to search in DB | 59 | // @username -> username to search in DB |
60 | if (query.search.startsWith('@')) query.search = query.search.replace(/^@/, '') | 60 | if (search.startsWith('@')) search = search.replace(/^@/, '') |
61 | 61 | ||
62 | if (isSearchIndexSearch(query)) { | 62 | if (isSearchIndexSearch(query)) { |
63 | return searchVideoChannelsIndex(query, res) | 63 | return searchVideoChannelsIndex(query, res) |
@@ -99,7 +99,8 @@ async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: expr | |||
99 | start: query.start, | 99 | start: query.start, |
100 | count: query.count, | 100 | count: query.count, |
101 | sort: query.sort, | 101 | sort: query.sort, |
102 | host: query.host | 102 | host: query.host, |
103 | names: query.names | ||
103 | }, 'filter:api.search.video-channels.local.list.params') | 104 | }, 'filter:api.search.video-channels.local.list.params') |
104 | 105 | ||
105 | const resultList = await Hooks.wrapPromiseFun( | 106 | const resultList = await Hooks.wrapPromiseFun( |
diff --git a/server/controllers/api/search/search-video-playlists.ts b/server/controllers/api/search/search-video-playlists.ts index 60d1a44f7..bd6a2a564 100644 --- a/server/controllers/api/search/search-video-playlists.ts +++ b/server/controllers/api/search/search-video-playlists.ts | |||
@@ -89,7 +89,8 @@ async function searchVideoPlaylistsDB (query: VideoPlaylistsSearchQuery, res: ex | |||
89 | start: query.start, | 89 | start: query.start, |
90 | count: query.count, | 90 | count: query.count, |
91 | sort: query.sort, | 91 | sort: query.sort, |
92 | host: query.host | 92 | host: query.host, |
93 | uuids: query.uuids | ||
93 | }, 'filter:api.search.video-playlists.local.list.params') | 94 | }, 'filter:api.search.video-playlists.local.list.params') |
94 | 95 | ||
95 | const resultList = await Hooks.wrapPromiseFun( | 96 | const resultList = await Hooks.wrapPromiseFun( |
diff --git a/server/helpers/custom-validators/misc.ts b/server/helpers/custom-validators/misc.ts index 528bfcfb8..f8f168149 100644 --- a/server/helpers/custom-validators/misc.ts +++ b/server/helpers/custom-validators/misc.ts | |||
@@ -39,6 +39,10 @@ function isUUIDValid (value: string) { | |||
39 | return exists(value) && validator.isUUID('' + value, 4) | 39 | return exists(value) && validator.isUUID('' + value, 4) |
40 | } | 40 | } |
41 | 41 | ||
42 | function areUUIDsValid (values: string[]) { | ||
43 | return isArray(values) && values.every(v => isUUIDValid(v)) | ||
44 | } | ||
45 | |||
42 | function isIdOrUUIDValid (value: string) { | 46 | function isIdOrUUIDValid (value: string) { |
43 | return isIdValid(value) || isUUIDValid(value) | 47 | return isIdValid(value) || isUUIDValid(value) |
44 | } | 48 | } |
@@ -132,6 +136,10 @@ function toCompleteUUID (value: string) { | |||
132 | return value | 136 | return value |
133 | } | 137 | } |
134 | 138 | ||
139 | function toCompleteUUIDs (values: string[]) { | ||
140 | return values.map(v => toCompleteUUID(v)) | ||
141 | } | ||
142 | |||
135 | function toIntOrNull (value: string) { | 143 | function toIntOrNull (value: string) { |
136 | const v = toValueOrNull(value) | 144 | const v = toValueOrNull(value) |
137 | 145 | ||
@@ -180,6 +188,7 @@ export { | |||
180 | isIdValid, | 188 | isIdValid, |
181 | isSafePath, | 189 | isSafePath, |
182 | isUUIDValid, | 190 | isUUIDValid, |
191 | toCompleteUUIDs, | ||
183 | toCompleteUUID, | 192 | toCompleteUUID, |
184 | isIdOrUUIDValid, | 193 | isIdOrUUIDValid, |
185 | isDateValid, | 194 | isDateValid, |
@@ -187,6 +196,7 @@ export { | |||
187 | toBooleanOrNull, | 196 | toBooleanOrNull, |
188 | isBooleanValid, | 197 | isBooleanValid, |
189 | toIntOrNull, | 198 | toIntOrNull, |
199 | areUUIDsValid, | ||
190 | toArray, | 200 | toArray, |
191 | toIntArray, | 201 | toIntArray, |
192 | isFileFieldValid, | 202 | isFileFieldValid, |
diff --git a/server/middlewares/validators/search.ts b/server/middlewares/validators/search.ts index ea6a490b2..cde300968 100644 --- a/server/middlewares/validators/search.ts +++ b/server/middlewares/validators/search.ts | |||
@@ -2,7 +2,7 @@ import * as express from 'express' | |||
2 | import { query } from 'express-validator' | 2 | import { query } from 'express-validator' |
3 | import { isSearchTargetValid } from '@server/helpers/custom-validators/search' | 3 | import { isSearchTargetValid } from '@server/helpers/custom-validators/search' |
4 | import { isHostValid } from '@server/helpers/custom-validators/servers' | 4 | import { isHostValid } from '@server/helpers/custom-validators/servers' |
5 | import { isDateValid } from '../../helpers/custom-validators/misc' | 5 | import { areUUIDsValid, isDateValid, toCompleteUUIDs } from '../../helpers/custom-validators/misc' |
6 | import { logger } from '../../helpers/logger' | 6 | import { logger } from '../../helpers/logger' |
7 | import { areValidationErrors } from './shared' | 7 | import { areValidationErrors } from './shared' |
8 | 8 | ||
@@ -27,8 +27,18 @@ const videosSearchValidator = [ | |||
27 | .optional() | 27 | .optional() |
28 | .custom(isDateValid).withMessage('Should have a published end date that conforms to ISO 8601'), | 28 | .custom(isDateValid).withMessage('Should have a published end date that conforms to ISO 8601'), |
29 | 29 | ||
30 | query('durationMin').optional().isInt().withMessage('Should have a valid min duration'), | 30 | query('durationMin') |
31 | query('durationMax').optional().isInt().withMessage('Should have a valid max duration'), | 31 | .optional() |
32 | .isInt().withMessage('Should have a valid min duration'), | ||
33 | query('durationMax') | ||
34 | .optional() | ||
35 | .isInt().withMessage('Should have a valid max duration'), | ||
36 | |||
37 | query('uuids') | ||
38 | .optional() | ||
39 | .toArray() | ||
40 | .customSanitizer(toCompleteUUIDs) | ||
41 | .custom(areUUIDsValid).withMessage('Should have valid uuids'), | ||
32 | 42 | ||
33 | query('searchTarget').optional().custom(isSearchTargetValid).withMessage('Should have a valid search target'), | 43 | query('searchTarget').optional().custom(isSearchTargetValid).withMessage('Should have a valid search target'), |
34 | 44 | ||
@@ -42,7 +52,9 @@ const videosSearchValidator = [ | |||
42 | ] | 52 | ] |
43 | 53 | ||
44 | const videoChannelsListSearchValidator = [ | 54 | const videoChannelsListSearchValidator = [ |
45 | query('search').not().isEmpty().withMessage('Should have a valid search'), | 55 | query('search') |
56 | .optional() | ||
57 | .not().isEmpty().withMessage('Should have a valid search'), | ||
46 | 58 | ||
47 | query('host') | 59 | query('host') |
48 | .optional() | 60 | .optional() |
@@ -52,6 +64,10 @@ const videoChannelsListSearchValidator = [ | |||
52 | .optional() | 64 | .optional() |
53 | .custom(isSearchTargetValid).withMessage('Should have a valid search target'), | 65 | .custom(isSearchTargetValid).withMessage('Should have a valid search target'), |
54 | 66 | ||
67 | query('names') | ||
68 | .optional() | ||
69 | .toArray(), | ||
70 | |||
55 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | 71 | (req: express.Request, res: express.Response, next: express.NextFunction) => { |
56 | logger.debug('Checking video channels search query', { parameters: req.query }) | 72 | logger.debug('Checking video channels search query', { parameters: req.query }) |
57 | 73 | ||
@@ -62,7 +78,9 @@ const videoChannelsListSearchValidator = [ | |||
62 | ] | 78 | ] |
63 | 79 | ||
64 | const videoPlaylistsListSearchValidator = [ | 80 | const videoPlaylistsListSearchValidator = [ |
65 | query('search').not().isEmpty().withMessage('Should have a valid search'), | 81 | query('search') |
82 | .optional() | ||
83 | .not().isEmpty().withMessage('Should have a valid search'), | ||
66 | 84 | ||
67 | query('host') | 85 | query('host') |
68 | .optional() | 86 | .optional() |
@@ -72,6 +90,12 @@ const videoPlaylistsListSearchValidator = [ | |||
72 | .optional() | 90 | .optional() |
73 | .custom(isSearchTargetValid).withMessage('Should have a valid search target'), | 91 | .custom(isSearchTargetValid).withMessage('Should have a valid search target'), |
74 | 92 | ||
93 | query('uuids') | ||
94 | .optional() | ||
95 | .toArray() | ||
96 | .customSanitizer(toCompleteUUIDs) | ||
97 | .custom(areUUIDsValid).withMessage('Should have valid uuids'), | ||
98 | |||
75 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | 99 | (req: express.Request, res: express.Response, next: express.NextFunction) => { |
76 | logger.debug('Checking video playlists search query', { parameters: req.query }) | 100 | logger.debug('Checking video playlists search query', { parameters: req.query }) |
77 | 101 | ||
diff --git a/server/models/video/sql/videos-id-list-query-builder.ts b/server/models/video/sql/videos-id-list-query-builder.ts index d4260c69c..7625c003d 100644 --- a/server/models/video/sql/videos-id-list-query-builder.ts +++ b/server/models/video/sql/videos-id-list-query-builder.ts | |||
@@ -35,6 +35,8 @@ export type BuildVideosListQueryOptions = { | |||
35 | tagsOneOf?: string[] | 35 | tagsOneOf?: string[] |
36 | tagsAllOf?: string[] | 36 | tagsAllOf?: string[] |
37 | 37 | ||
38 | uuids?: string[] | ||
39 | |||
38 | withFiles?: boolean | 40 | withFiles?: boolean |
39 | 41 | ||
40 | accountId?: number | 42 | accountId?: number |
@@ -161,6 +163,10 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder { | |||
161 | this.whereTagsAllOf(options.tagsAllOf) | 163 | this.whereTagsAllOf(options.tagsAllOf) |
162 | } | 164 | } |
163 | 165 | ||
166 | if (options.uuids) { | ||
167 | this.whereUUIDs(options.uuids) | ||
168 | } | ||
169 | |||
164 | if (options.nsfw === true) { | 170 | if (options.nsfw === true) { |
165 | this.whereNSFW() | 171 | this.whereNSFW() |
166 | } else if (options.nsfw === false) { | 172 | } else if (options.nsfw === false) { |
@@ -386,6 +392,10 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder { | |||
386 | ) | 392 | ) |
387 | } | 393 | } |
388 | 394 | ||
395 | private whereUUIDs (uuids: string[]) { | ||
396 | this.and.push('"video"."uuid" IN (' + createSafeIn(this.sequelize, uuids) + ')') | ||
397 | } | ||
398 | |||
389 | private whereCategoryOneOf (categoryOneOf: number[]) { | 399 | private whereCategoryOneOf (categoryOneOf: number[]) { |
390 | this.and.push('"video"."category" IN (:categoryOneOf)') | 400 | this.and.push('"video"."category" IN (:categoryOneOf)') |
391 | this.replacements.categoryOneOf = categoryOneOf | 401 | this.replacements.categoryOneOf = categoryOneOf |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 9aa271711..327f49304 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -59,6 +59,7 @@ type AvailableForListOptions = { | |||
59 | actorId: number | 59 | actorId: number |
60 | search?: string | 60 | search?: string |
61 | host?: string | 61 | host?: string |
62 | names?: string[] | ||
62 | } | 63 | } |
63 | 64 | ||
64 | type AvailableWithStatsOptions = { | 65 | type AvailableWithStatsOptions = { |
@@ -84,18 +85,20 @@ export type SummaryOptions = { | |||
84 | // Only list local channels OR channels that are on an instance followed by actorId | 85 | // Only list local channels OR channels that are on an instance followed by actorId |
85 | const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId) | 86 | const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId) |
86 | 87 | ||
87 | const whereActor = { | 88 | const whereActorAnd: WhereOptions[] = [ |
88 | [Op.or]: [ | 89 | { |
89 | { | 90 | [Op.or]: [ |
90 | serverId: null | 91 | { |
91 | }, | 92 | serverId: null |
92 | { | 93 | }, |
93 | serverId: { | 94 | { |
94 | [Op.in]: Sequelize.literal(inQueryInstanceFollow) | 95 | serverId: { |
96 | [Op.in]: Sequelize.literal(inQueryInstanceFollow) | ||
97 | } | ||
95 | } | 98 | } |
96 | } | 99 | ] |
97 | ] | 100 | } |
98 | } | 101 | ] |
99 | 102 | ||
100 | let serverRequired = false | 103 | let serverRequired = false |
101 | let whereServer: WhereOptions | 104 | let whereServer: WhereOptions |
@@ -106,8 +109,16 @@ export type SummaryOptions = { | |||
106 | } | 109 | } |
107 | 110 | ||
108 | if (options.host === WEBSERVER.HOST) { | 111 | if (options.host === WEBSERVER.HOST) { |
109 | Object.assign(whereActor, { | 112 | whereActorAnd.push({ |
110 | [Op.and]: [ { serverId: null } ] | 113 | serverId: null |
114 | }) | ||
115 | } | ||
116 | |||
117 | if (options.names) { | ||
118 | whereActorAnd.push({ | ||
119 | preferredUsername: { | ||
120 | [Op.in]: options.names | ||
121 | } | ||
111 | }) | 122 | }) |
112 | } | 123 | } |
113 | 124 | ||
@@ -118,7 +129,9 @@ export type SummaryOptions = { | |||
118 | exclude: unusedActorAttributesForAPI | 129 | exclude: unusedActorAttributesForAPI |
119 | }, | 130 | }, |
120 | model: ActorModel, | 131 | model: ActorModel, |
121 | where: whereActor, | 132 | where: { |
133 | [Op.and]: whereActorAnd | ||
134 | }, | ||
122 | include: [ | 135 | include: [ |
123 | { | 136 | { |
124 | model: ServerModel, | 137 | model: ServerModel, |
@@ -454,26 +467,23 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
454 | 467 | ||
455 | static searchForApi (options: { | 468 | static searchForApi (options: { |
456 | actorId: number | 469 | actorId: number |
457 | search: string | 470 | search?: string |
458 | start: number | 471 | start: number |
459 | count: number | 472 | count: number |
460 | sort: string | 473 | sort: string |
461 | 474 | ||
462 | host?: string | 475 | host?: string |
476 | names?: string[] | ||
463 | }) { | 477 | }) { |
464 | const attributesInclude = [] | 478 | let attributesInclude: any[] = [ literal('0 as similarity') ] |
465 | const escapedSearch = VideoChannelModel.sequelize.escape(options.search) | 479 | let where: WhereOptions |
466 | const escapedLikeSearch = VideoChannelModel.sequelize.escape('%' + options.search + '%') | ||
467 | attributesInclude.push(createSimilarityAttribute('VideoChannelModel.name', options.search)) | ||
468 | 480 | ||
469 | const query = { | 481 | if (options.search) { |
470 | attributes: { | 482 | const escapedSearch = VideoChannelModel.sequelize.escape(options.search) |
471 | include: attributesInclude | 483 | const escapedLikeSearch = VideoChannelModel.sequelize.escape('%' + options.search + '%') |
472 | }, | 484 | attributesInclude = [ createSimilarityAttribute('VideoChannelModel.name', options.search) ] |
473 | offset: options.start, | 485 | |
474 | limit: options.count, | 486 | where = { |
475 | order: getSort(options.sort), | ||
476 | where: { | ||
477 | [Op.or]: [ | 487 | [Op.or]: [ |
478 | Sequelize.literal( | 488 | Sequelize.literal( |
479 | 'lower(immutable_unaccent("VideoChannelModel"."name")) % lower(immutable_unaccent(' + escapedSearch + '))' | 489 | 'lower(immutable_unaccent("VideoChannelModel"."name")) % lower(immutable_unaccent(' + escapedSearch + '))' |
@@ -485,9 +495,19 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
485 | } | 495 | } |
486 | } | 496 | } |
487 | 497 | ||
498 | const query = { | ||
499 | attributes: { | ||
500 | include: attributesInclude | ||
501 | }, | ||
502 | offset: options.start, | ||
503 | limit: options.count, | ||
504 | order: getSort(options.sort), | ||
505 | where | ||
506 | } | ||
507 | |||
488 | return VideoChannelModel | 508 | return VideoChannelModel |
489 | .scope({ | 509 | .scope({ |
490 | method: [ ScopeNames.FOR_API, { actorId: options.actorId, host: options.host } as AvailableForListOptions ] | 510 | method: [ ScopeNames.FOR_API, { actorId: options.actorId, host: options.host, names: options.names } as AvailableForListOptions ] |
491 | }) | 511 | }) |
492 | .findAndCountAll(query) | 512 | .findAndCountAll(query) |
493 | .then(({ rows, count }) => { | 513 | .then(({ rows, count }) => { |
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index a2dc7075d..caa79952d 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts | |||
@@ -83,6 +83,7 @@ type AvailableForListOptions = { | |||
83 | listMyPlaylists?: boolean | 83 | listMyPlaylists?: boolean |
84 | search?: string | 84 | search?: string |
85 | host?: string | 85 | host?: string |
86 | uuids?: string[] | ||
86 | withVideos?: boolean | 87 | withVideos?: boolean |
87 | } | 88 | } |
88 | 89 | ||
@@ -200,18 +201,26 @@ function getVideoLengthSelect () { | |||
200 | }) | 201 | }) |
201 | } | 202 | } |
202 | 203 | ||
204 | if (options.uuids) { | ||
205 | whereAnd.push({ | ||
206 | uuid: { | ||
207 | [Op.in]: options.uuids | ||
208 | } | ||
209 | }) | ||
210 | } | ||
211 | |||
203 | if (options.withVideos === true) { | 212 | if (options.withVideos === true) { |
204 | whereAnd.push( | 213 | whereAnd.push( |
205 | literal(`(${getVideoLengthSelect()}) != 0`) | 214 | literal(`(${getVideoLengthSelect()}) != 0`) |
206 | ) | 215 | ) |
207 | } | 216 | } |
208 | 217 | ||
209 | const attributesInclude = [] | 218 | let attributesInclude: any[] = [ literal('0 as similarity') ] |
210 | 219 | ||
211 | if (options.search) { | 220 | if (options.search) { |
212 | const escapedSearch = VideoPlaylistModel.sequelize.escape(options.search) | 221 | const escapedSearch = VideoPlaylistModel.sequelize.escape(options.search) |
213 | const escapedLikeSearch = VideoPlaylistModel.sequelize.escape('%' + options.search + '%') | 222 | const escapedLikeSearch = VideoPlaylistModel.sequelize.escape('%' + options.search + '%') |
214 | attributesInclude.push(createSimilarityAttribute('VideoPlaylistModel.name', options.search)) | 223 | attributesInclude = [ createSimilarityAttribute('VideoPlaylistModel.name', options.search) ] |
215 | 224 | ||
216 | whereAnd.push({ | 225 | whereAnd.push({ |
217 | [Op.or]: [ | 226 | [Op.or]: [ |
@@ -359,6 +368,7 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
359 | listMyPlaylists?: boolean | 368 | listMyPlaylists?: boolean |
360 | search?: string | 369 | search?: string |
361 | host?: string | 370 | host?: string |
371 | uuids?: string[] | ||
362 | withVideos?: boolean // false by default | 372 | withVideos?: boolean // false by default |
363 | }) { | 373 | }) { |
364 | const query = { | 374 | const query = { |
@@ -379,6 +389,7 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
379 | listMyPlaylists: options.listMyPlaylists, | 389 | listMyPlaylists: options.listMyPlaylists, |
380 | search: options.search, | 390 | search: options.search, |
381 | host: options.host, | 391 | host: options.host, |
392 | uuids: options.uuids, | ||
382 | withVideos: options.withVideos || false | 393 | withVideos: options.withVideos || false |
383 | } as AvailableForListOptions | 394 | } as AvailableForListOptions |
384 | ] | 395 | ] |
@@ -402,6 +413,7 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
402 | sort: string | 413 | sort: string |
403 | search?: string | 414 | search?: string |
404 | host?: string | 415 | host?: string |
416 | uuids?: string[] | ||
405 | }) { | 417 | }) { |
406 | return VideoPlaylistModel.listForApi({ | 418 | return VideoPlaylistModel.listForApi({ |
407 | ...options, | 419 | ...options, |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index c444f381e..fe92ead04 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -1132,6 +1132,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1132 | durationMax?: number // seconds | 1132 | durationMax?: number // seconds |
1133 | user?: MUserAccountId | 1133 | user?: MUserAccountId |
1134 | filter?: VideoFilter | 1134 | filter?: VideoFilter |
1135 | uuids?: string[] | ||
1135 | }) { | 1136 | }) { |
1136 | const serverActor = await getServerActor() | 1137 | const serverActor = await getServerActor() |
1137 | 1138 | ||
@@ -1167,6 +1168,8 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1167 | durationMin: options.durationMin, | 1168 | durationMin: options.durationMin, |
1168 | durationMax: options.durationMax, | 1169 | durationMax: options.durationMax, |
1169 | 1170 | ||
1171 | uuids: options.uuids, | ||
1172 | |||
1170 | search: options.search | 1173 | search: options.search |
1171 | } | 1174 | } |
1172 | 1175 | ||
diff --git a/server/tests/api/check-params/search.ts b/server/tests/api/check-params/search.ts index 72ad6c842..789ea7754 100644 --- a/server/tests/api/check-params/search.ts +++ b/server/tests/api/check-params/search.ts | |||
@@ -146,6 +146,16 @@ describe('Test videos API validator', function () { | |||
146 | const customQuery = { ...query, host: 'example.com' } | 146 | const customQuery = { ...query, host: 'example.com' } |
147 | await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 }) | 147 | await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 }) |
148 | }) | 148 | }) |
149 | |||
150 | it('Should fail with invalid uuids', async function () { | ||
151 | const customQuery = { ...query, uuids: [ '6565', 'dfd70b83-639f-4980-94af-304a56ab4b35' ] } | ||
152 | await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
153 | }) | ||
154 | |||
155 | it('Should succeed with valid uuids', async function () { | ||
156 | const customQuery = { ...query, uuids: [ 'dfd70b83-639f-4980-94af-304a56ab4b35' ] } | ||
157 | await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 }) | ||
158 | }) | ||
149 | }) | 159 | }) |
150 | 160 | ||
151 | describe('When searching video playlists', function () { | 161 | describe('When searching video playlists', function () { |
@@ -172,6 +182,11 @@ describe('Test videos API validator', function () { | |||
172 | await makeGetRequest({ url: server.url, path, query: { ...query, host: '6565' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 182 | await makeGetRequest({ url: server.url, path, query: { ...query, host: '6565' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
173 | }) | 183 | }) |
174 | 184 | ||
185 | it('Should fail with invalid uuids', async function () { | ||
186 | const customQuery = { ...query, uuids: [ '6565', 'dfd70b83-639f-4980-94af-304a56ab4b35' ] } | ||
187 | await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
188 | }) | ||
189 | |||
175 | it('Should succeed with the correct parameters', async function () { | 190 | it('Should succeed with the correct parameters', async function () { |
176 | await makeGetRequest({ url: server.url, path, query, expectedStatus: HttpStatusCode.OK_200 }) | 191 | await makeGetRequest({ url: server.url, path, query, expectedStatus: HttpStatusCode.OK_200 }) |
177 | }) | 192 | }) |
diff --git a/server/tests/api/search/search-channels.ts b/server/tests/api/search/search-channels.ts index aab03bfd1..ef78c0f67 100644 --- a/server/tests/api/search/search-channels.ts +++ b/server/tests/api/search/search-channels.ts | |||
@@ -22,8 +22,12 @@ describe('Test channels search', function () { | |||
22 | before(async function () { | 22 | before(async function () { |
23 | this.timeout(120000) | 23 | this.timeout(120000) |
24 | 24 | ||
25 | server = await createSingleServer(1) | 25 | const servers = await Promise.all([ |
26 | remoteServer = await createSingleServer(2, { transcoding: { enabled: false } }) | 26 | createSingleServer(1), |
27 | createSingleServer(2, { transcoding: { enabled: false } }) | ||
28 | ]) | ||
29 | server = servers[0] | ||
30 | remoteServer = servers[1] | ||
27 | 31 | ||
28 | await setAccessTokensToServers([ server, remoteServer ]) | 32 | await setAccessTokensToServers([ server, remoteServer ]) |
29 | 33 | ||
@@ -116,6 +120,22 @@ describe('Test channels search', function () { | |||
116 | } | 120 | } |
117 | }) | 121 | }) |
118 | 122 | ||
123 | it('Should filter by names', async function () { | ||
124 | { | ||
125 | const body = await command.advancedChannelSearch({ search: { names: [ 'squall_channel', 'zell_channel' ] } }) | ||
126 | expect(body.total).to.equal(2) | ||
127 | expect(body.data).to.have.lengthOf(2) | ||
128 | expect(body.data[0].displayName).to.equal('Squall channel') | ||
129 | expect(body.data[1].displayName).to.equal('Zell channel') | ||
130 | } | ||
131 | |||
132 | { | ||
133 | const body = await command.advancedChannelSearch({ search: { names: [ 'chocobozzz_channel' ] } }) | ||
134 | expect(body.total).to.equal(0) | ||
135 | expect(body.data).to.have.lengthOf(0) | ||
136 | } | ||
137 | }) | ||
138 | |||
119 | after(async function () { | 139 | after(async function () { |
120 | await cleanupTests([ server ]) | 140 | await cleanupTests([ server ]) |
121 | }) | 141 | }) |
diff --git a/server/tests/api/search/search-playlists.ts b/server/tests/api/search/search-playlists.ts index e7e53ff41..85be1eb59 100644 --- a/server/tests/api/search/search-playlists.ts +++ b/server/tests/api/search/search-playlists.ts | |||
@@ -19,12 +19,18 @@ describe('Test playlists search', function () { | |||
19 | let server: PeerTubeServer | 19 | let server: PeerTubeServer |
20 | let remoteServer: PeerTubeServer | 20 | let remoteServer: PeerTubeServer |
21 | let command: SearchCommand | 21 | let command: SearchCommand |
22 | let playlistUUID: string | ||
23 | let playlistShortUUID: string | ||
22 | 24 | ||
23 | before(async function () { | 25 | before(async function () { |
24 | this.timeout(120000) | 26 | this.timeout(120000) |
25 | 27 | ||
26 | server = await createSingleServer(1) | 28 | const servers = await Promise.all([ |
27 | remoteServer = await createSingleServer(2, { transcoding: { enabled: false } }) | 29 | createSingleServer(1), |
30 | createSingleServer(2, { transcoding: { enabled: false } }) | ||
31 | ]) | ||
32 | server = servers[0] | ||
33 | remoteServer = servers[1] | ||
28 | 34 | ||
29 | await setAccessTokensToServers([ remoteServer, server ]) | 35 | await setAccessTokensToServers([ remoteServer, server ]) |
30 | await setDefaultVideoChannel([ remoteServer, server ]) | 36 | await setDefaultVideoChannel([ remoteServer, server ]) |
@@ -38,6 +44,8 @@ describe('Test playlists search', function () { | |||
38 | videoChannelId: server.store.channel.id | 44 | videoChannelId: server.store.channel.id |
39 | } | 45 | } |
40 | const created = await server.playlists.create({ attributes }) | 46 | const created = await server.playlists.create({ attributes }) |
47 | playlistUUID = created.uuid | ||
48 | playlistShortUUID = created.shortUUID | ||
41 | 49 | ||
42 | await server.playlists.addElement({ playlistId: created.id, attributes: { videoId } }) | 50 | await server.playlists.addElement({ playlistId: created.id, attributes: { videoId } }) |
43 | } | 51 | } |
@@ -136,6 +144,22 @@ describe('Test playlists search', function () { | |||
136 | } | 144 | } |
137 | }) | 145 | }) |
138 | 146 | ||
147 | it('Should filter by UUIDs', async function () { | ||
148 | for (const uuid of [ playlistUUID, playlistShortUUID ]) { | ||
149 | const body = await command.advancedPlaylistSearch({ search: { uuids: [ uuid ] } }) | ||
150 | |||
151 | expect(body.total).to.equal(1) | ||
152 | expect(body.data[0].displayName).to.equal('Dr. Kenzo Tenma hospital videos') | ||
153 | } | ||
154 | |||
155 | { | ||
156 | const body = await command.advancedPlaylistSearch({ search: { uuids: [ 'dfd70b83-639f-4980-94af-304a56ab4b35' ] } }) | ||
157 | |||
158 | expect(body.total).to.equal(0) | ||
159 | expect(body.data).to.have.lengthOf(0) | ||
160 | } | ||
161 | }) | ||
162 | |||
139 | it('Should not display playlists without videos', async function () { | 163 | it('Should not display playlists without videos', async function () { |
140 | const search = { | 164 | const search = { |
141 | search: 'Lunge', | 165 | search: 'Lunge', |
diff --git a/server/tests/api/search/search-videos.ts b/server/tests/api/search/search-videos.ts index a56dc1d87..bd1e4d266 100644 --- a/server/tests/api/search/search-videos.ts +++ b/server/tests/api/search/search-videos.ts | |||
@@ -22,14 +22,19 @@ describe('Test videos search', function () { | |||
22 | let remoteServer: PeerTubeServer | 22 | let remoteServer: PeerTubeServer |
23 | let startDate: string | 23 | let startDate: string |
24 | let videoUUID: string | 24 | let videoUUID: string |
25 | let videoShortUUID: string | ||
25 | 26 | ||
26 | let command: SearchCommand | 27 | let command: SearchCommand |
27 | 28 | ||
28 | before(async function () { | 29 | before(async function () { |
29 | this.timeout(120000) | 30 | this.timeout(120000) |
30 | 31 | ||
31 | server = await createSingleServer(1) | 32 | const servers = await Promise.all([ |
32 | remoteServer = await createSingleServer(2) | 33 | createSingleServer(1), |
34 | createSingleServer(2) | ||
35 | ]) | ||
36 | server = servers[0] | ||
37 | remoteServer = servers[1] | ||
33 | 38 | ||
34 | await setAccessTokensToServers([ server, remoteServer ]) | 39 | await setAccessTokensToServers([ server, remoteServer ]) |
35 | await setDefaultVideoChannel([ server, remoteServer ]) | 40 | await setDefaultVideoChannel([ server, remoteServer ]) |
@@ -50,8 +55,9 @@ describe('Test videos search', function () { | |||
50 | 55 | ||
51 | { | 56 | { |
52 | const attributes3 = { ...attributes1, name: attributes1.name + ' - 3', language: undefined } | 57 | const attributes3 = { ...attributes1, name: attributes1.name + ' - 3', language: undefined } |
53 | const { id, uuid } = await server.videos.upload({ attributes: attributes3 }) | 58 | const { id, uuid, shortUUID } = await server.videos.upload({ attributes: attributes3 }) |
54 | videoUUID = uuid | 59 | videoUUID = uuid |
60 | videoShortUUID = shortUUID | ||
55 | 61 | ||
56 | await server.captions.add({ | 62 | await server.captions.add({ |
57 | language: 'en', | 63 | language: 'en', |
@@ -479,6 +485,22 @@ describe('Test videos search', function () { | |||
479 | expect(body.data[0].name).to.equal('1111 2222 3333 - 3') | 485 | expect(body.data[0].name).to.equal('1111 2222 3333 - 3') |
480 | }) | 486 | }) |
481 | 487 | ||
488 | it('Should filter by UUIDs', async function () { | ||
489 | for (const uuid of [ videoUUID, videoShortUUID ]) { | ||
490 | const body = await command.advancedVideoSearch({ search: { uuids: [ uuid ] } }) | ||
491 | |||
492 | expect(body.total).to.equal(1) | ||
493 | expect(body.data[0].name).to.equal('1111 2222 3333 - 3') | ||
494 | } | ||
495 | |||
496 | { | ||
497 | const body = await command.advancedVideoSearch({ search: { uuids: [ 'dfd70b83-639f-4980-94af-304a56ab4b35' ] } }) | ||
498 | |||
499 | expect(body.total).to.equal(0) | ||
500 | expect(body.data).to.have.lengthOf(0) | ||
501 | } | ||
502 | }) | ||
503 | |||
482 | it('Should search by host', async function () { | 504 | it('Should search by host', async function () { |
483 | { | 505 | { |
484 | const body = await command.advancedVideoSearch({ search: { search: '6666 7777 8888', host: server.host } }) | 506 | const body = await command.advancedVideoSearch({ search: { search: '6666 7777 8888', host: server.host } }) |
diff --git a/shared/models/search/video-channels-search-query.model.ts b/shared/models/search/video-channels-search-query.model.ts index 2622dfbc6..50c59d41d 100644 --- a/shared/models/search/video-channels-search-query.model.ts +++ b/shared/models/search/video-channels-search-query.model.ts | |||
@@ -1,11 +1,12 @@ | |||
1 | import { SearchTargetQuery } from './search-target-query.model' | 1 | import { SearchTargetQuery } from './search-target-query.model' |
2 | 2 | ||
3 | export interface VideoChannelsSearchQuery extends SearchTargetQuery { | 3 | export interface VideoChannelsSearchQuery extends SearchTargetQuery { |
4 | search: string | 4 | search?: string |
5 | 5 | ||
6 | start?: number | 6 | start?: number |
7 | count?: number | 7 | count?: number |
8 | sort?: string | 8 | sort?: string |
9 | 9 | ||
10 | host?: string | 10 | host?: string |
11 | names?: string[] | ||
11 | } | 12 | } |
diff --git a/shared/models/search/video-playlists-search-query.model.ts b/shared/models/search/video-playlists-search-query.model.ts index dcf66e9e3..55393c92a 100644 --- a/shared/models/search/video-playlists-search-query.model.ts +++ b/shared/models/search/video-playlists-search-query.model.ts | |||
@@ -1,11 +1,12 @@ | |||
1 | import { SearchTargetQuery } from './search-target-query.model' | 1 | import { SearchTargetQuery } from './search-target-query.model' |
2 | 2 | ||
3 | export interface VideoPlaylistsSearchQuery extends SearchTargetQuery { | 3 | export interface VideoPlaylistsSearchQuery extends SearchTargetQuery { |
4 | search: string | 4 | search?: string |
5 | 5 | ||
6 | start?: number | 6 | start?: number |
7 | count?: number | 7 | count?: number |
8 | sort?: string | 8 | sort?: string |
9 | 9 | ||
10 | host?: string | 10 | host?: string |
11 | uuids?: string[] | ||
11 | } | 12 | } |
diff --git a/shared/models/search/videos-search-query.model.ts b/shared/models/search/videos-search-query.model.ts index a568c960e..736d89577 100644 --- a/shared/models/search/videos-search-query.model.ts +++ b/shared/models/search/videos-search-query.model.ts | |||
@@ -14,4 +14,7 @@ export interface VideosSearchQuery extends SearchTargetQuery, VideosCommonQuery | |||
14 | 14 | ||
15 | durationMin?: number // seconds | 15 | durationMin?: number // seconds |
16 | durationMax?: number // seconds | 16 | durationMax?: number // seconds |
17 | |||
18 | // UUIDs or short | ||
19 | uuids?: string[] | ||
17 | } | 20 | } |