1
0
Fork 0

Add ability to search by uuids/actor names

This commit is contained in:
Chocobozzz 2021-07-28 16:40:21 +02:00
parent 164c8d46cf
commit fbd67e7f38
No known key found for this signature in database
GPG key ID: 583A612D890159BE
15 changed files with 215 additions and 48 deletions

View file

@ -46,7 +46,7 @@ export { searchChannelsRouter }
function searchVideoChannels (req: express.Request, res: express.Response) { function searchVideoChannels (req: express.Request, res: express.Response) {
const query: VideoChannelsSearchQuery = req.query const query: VideoChannelsSearchQuery = req.query
const search = query.search let search = query.search || ''
const parts = search.split('@') const parts = search.split('@')
@ -57,7 +57,7 @@ function searchVideoChannels (req: express.Request, res: express.Response) {
if (isURISearch(search) || isWebfingerSearch) return searchVideoChannelURI(search, isWebfingerSearch, res) if (isURISearch(search) || isWebfingerSearch) return searchVideoChannelURI(search, isWebfingerSearch, res)
// @username -> username to search in DB // @username -> username to search in DB
if (query.search.startsWith('@')) query.search = query.search.replace(/^@/, '') if (search.startsWith('@')) search = search.replace(/^@/, '')
if (isSearchIndexSearch(query)) { if (isSearchIndexSearch(query)) {
return searchVideoChannelsIndex(query, res) return searchVideoChannelsIndex(query, res)
@ -99,7 +99,8 @@ async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: expr
start: query.start, start: query.start,
count: query.count, count: query.count,
sort: query.sort, sort: query.sort,
host: query.host host: query.host,
names: query.names
}, 'filter:api.search.video-channels.local.list.params') }, 'filter:api.search.video-channels.local.list.params')
const resultList = await Hooks.wrapPromiseFun( const resultList = await Hooks.wrapPromiseFun(

View file

@ -89,7 +89,8 @@ async function searchVideoPlaylistsDB (query: VideoPlaylistsSearchQuery, res: ex
start: query.start, start: query.start,
count: query.count, count: query.count,
sort: query.sort, sort: query.sort,
host: query.host host: query.host,
uuids: query.uuids
}, 'filter:api.search.video-playlists.local.list.params') }, 'filter:api.search.video-playlists.local.list.params')
const resultList = await Hooks.wrapPromiseFun( const resultList = await Hooks.wrapPromiseFun(

View file

@ -39,6 +39,10 @@ function isUUIDValid (value: string) {
return exists(value) && validator.isUUID('' + value, 4) return exists(value) && validator.isUUID('' + value, 4)
} }
function areUUIDsValid (values: string[]) {
return isArray(values) && values.every(v => isUUIDValid(v))
}
function isIdOrUUIDValid (value: string) { function isIdOrUUIDValid (value: string) {
return isIdValid(value) || isUUIDValid(value) return isIdValid(value) || isUUIDValid(value)
} }
@ -132,6 +136,10 @@ function toCompleteUUID (value: string) {
return value return value
} }
function toCompleteUUIDs (values: string[]) {
return values.map(v => toCompleteUUID(v))
}
function toIntOrNull (value: string) { function toIntOrNull (value: string) {
const v = toValueOrNull(value) const v = toValueOrNull(value)
@ -180,6 +188,7 @@ export {
isIdValid, isIdValid,
isSafePath, isSafePath,
isUUIDValid, isUUIDValid,
toCompleteUUIDs,
toCompleteUUID, toCompleteUUID,
isIdOrUUIDValid, isIdOrUUIDValid,
isDateValid, isDateValid,
@ -187,6 +196,7 @@ export {
toBooleanOrNull, toBooleanOrNull,
isBooleanValid, isBooleanValid,
toIntOrNull, toIntOrNull,
areUUIDsValid,
toArray, toArray,
toIntArray, toIntArray,
isFileFieldValid, isFileFieldValid,

View file

@ -2,7 +2,7 @@ import * as express from 'express'
import { query } from 'express-validator' import { query } from 'express-validator'
import { isSearchTargetValid } from '@server/helpers/custom-validators/search' import { isSearchTargetValid } from '@server/helpers/custom-validators/search'
import { isHostValid } from '@server/helpers/custom-validators/servers' import { isHostValid } from '@server/helpers/custom-validators/servers'
import { isDateValid } from '../../helpers/custom-validators/misc' import { areUUIDsValid, isDateValid, toCompleteUUIDs } from '../../helpers/custom-validators/misc'
import { logger } from '../../helpers/logger' import { logger } from '../../helpers/logger'
import { areValidationErrors } from './shared' import { areValidationErrors } from './shared'
@ -27,8 +27,18 @@ const videosSearchValidator = [
.optional() .optional()
.custom(isDateValid).withMessage('Should have a published end date that conforms to ISO 8601'), .custom(isDateValid).withMessage('Should have a published end date that conforms to ISO 8601'),
query('durationMin').optional().isInt().withMessage('Should have a valid min duration'), query('durationMin')
query('durationMax').optional().isInt().withMessage('Should have a valid max duration'), .optional()
.isInt().withMessage('Should have a valid min duration'),
query('durationMax')
.optional()
.isInt().withMessage('Should have a valid max duration'),
query('uuids')
.optional()
.toArray()
.customSanitizer(toCompleteUUIDs)
.custom(areUUIDsValid).withMessage('Should have valid uuids'),
query('searchTarget').optional().custom(isSearchTargetValid).withMessage('Should have a valid search target'), query('searchTarget').optional().custom(isSearchTargetValid).withMessage('Should have a valid search target'),
@ -42,7 +52,9 @@ const videosSearchValidator = [
] ]
const videoChannelsListSearchValidator = [ const videoChannelsListSearchValidator = [
query('search').not().isEmpty().withMessage('Should have a valid search'), query('search')
.optional()
.not().isEmpty().withMessage('Should have a valid search'),
query('host') query('host')
.optional() .optional()
@ -52,6 +64,10 @@ const videoChannelsListSearchValidator = [
.optional() .optional()
.custom(isSearchTargetValid).withMessage('Should have a valid search target'), .custom(isSearchTargetValid).withMessage('Should have a valid search target'),
query('names')
.optional()
.toArray(),
(req: express.Request, res: express.Response, next: express.NextFunction) => { (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking video channels search query', { parameters: req.query }) logger.debug('Checking video channels search query', { parameters: req.query })
@ -62,7 +78,9 @@ const videoChannelsListSearchValidator = [
] ]
const videoPlaylistsListSearchValidator = [ const videoPlaylistsListSearchValidator = [
query('search').not().isEmpty().withMessage('Should have a valid search'), query('search')
.optional()
.not().isEmpty().withMessage('Should have a valid search'),
query('host') query('host')
.optional() .optional()
@ -72,6 +90,12 @@ const videoPlaylistsListSearchValidator = [
.optional() .optional()
.custom(isSearchTargetValid).withMessage('Should have a valid search target'), .custom(isSearchTargetValid).withMessage('Should have a valid search target'),
query('uuids')
.optional()
.toArray()
.customSanitizer(toCompleteUUIDs)
.custom(areUUIDsValid).withMessage('Should have valid uuids'),
(req: express.Request, res: express.Response, next: express.NextFunction) => { (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking video playlists search query', { parameters: req.query }) logger.debug('Checking video playlists search query', { parameters: req.query })

View file

@ -35,6 +35,8 @@ export type BuildVideosListQueryOptions = {
tagsOneOf?: string[] tagsOneOf?: string[]
tagsAllOf?: string[] tagsAllOf?: string[]
uuids?: string[]
withFiles?: boolean withFiles?: boolean
accountId?: number accountId?: number
@ -161,6 +163,10 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
this.whereTagsAllOf(options.tagsAllOf) this.whereTagsAllOf(options.tagsAllOf)
} }
if (options.uuids) {
this.whereUUIDs(options.uuids)
}
if (options.nsfw === true) { if (options.nsfw === true) {
this.whereNSFW() this.whereNSFW()
} else if (options.nsfw === false) { } else if (options.nsfw === false) {
@ -386,6 +392,10 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
) )
} }
private whereUUIDs (uuids: string[]) {
this.and.push('"video"."uuid" IN (' + createSafeIn(this.sequelize, uuids) + ')')
}
private whereCategoryOneOf (categoryOneOf: number[]) { private whereCategoryOneOf (categoryOneOf: number[]) {
this.and.push('"video"."category" IN (:categoryOneOf)') this.and.push('"video"."category" IN (:categoryOneOf)')
this.replacements.categoryOneOf = categoryOneOf this.replacements.categoryOneOf = categoryOneOf

View file

@ -59,6 +59,7 @@ type AvailableForListOptions = {
actorId: number actorId: number
search?: string search?: string
host?: string host?: string
names?: string[]
} }
type AvailableWithStatsOptions = { type AvailableWithStatsOptions = {
@ -84,18 +85,20 @@ export type SummaryOptions = {
// Only list local channels OR channels that are on an instance followed by actorId // Only list local channels OR channels that are on an instance followed by actorId
const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId) const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId)
const whereActor = { const whereActorAnd: WhereOptions[] = [
[Op.or]: [ {
{ [Op.or]: [
serverId: null {
}, serverId: null
{ },
serverId: { {
[Op.in]: Sequelize.literal(inQueryInstanceFollow) serverId: {
[Op.in]: Sequelize.literal(inQueryInstanceFollow)
}
} }
} ]
] }
} ]
let serverRequired = false let serverRequired = false
let whereServer: WhereOptions let whereServer: WhereOptions
@ -106,8 +109,16 @@ export type SummaryOptions = {
} }
if (options.host === WEBSERVER.HOST) { if (options.host === WEBSERVER.HOST) {
Object.assign(whereActor, { whereActorAnd.push({
[Op.and]: [ { serverId: null } ] serverId: null
})
}
if (options.names) {
whereActorAnd.push({
preferredUsername: {
[Op.in]: options.names
}
}) })
} }
@ -118,7 +129,9 @@ export type SummaryOptions = {
exclude: unusedActorAttributesForAPI exclude: unusedActorAttributesForAPI
}, },
model: ActorModel, model: ActorModel,
where: whereActor, where: {
[Op.and]: whereActorAnd
},
include: [ include: [
{ {
model: ServerModel, model: ServerModel,
@ -454,26 +467,23 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
static searchForApi (options: { static searchForApi (options: {
actorId: number actorId: number
search: string search?: string
start: number start: number
count: number count: number
sort: string sort: string
host?: string host?: string
names?: string[]
}) { }) {
const attributesInclude = [] let attributesInclude: any[] = [ literal('0 as similarity') ]
const escapedSearch = VideoChannelModel.sequelize.escape(options.search) let where: WhereOptions
const escapedLikeSearch = VideoChannelModel.sequelize.escape('%' + options.search + '%')
attributesInclude.push(createSimilarityAttribute('VideoChannelModel.name', options.search))
const query = { if (options.search) {
attributes: { const escapedSearch = VideoChannelModel.sequelize.escape(options.search)
include: attributesInclude const escapedLikeSearch = VideoChannelModel.sequelize.escape('%' + options.search + '%')
}, attributesInclude = [ createSimilarityAttribute('VideoChannelModel.name', options.search) ]
offset: options.start,
limit: options.count, where = {
order: getSort(options.sort),
where: {
[Op.or]: [ [Op.or]: [
Sequelize.literal( Sequelize.literal(
'lower(immutable_unaccent("VideoChannelModel"."name")) % lower(immutable_unaccent(' + escapedSearch + '))' 'lower(immutable_unaccent("VideoChannelModel"."name")) % lower(immutable_unaccent(' + escapedSearch + '))'
@ -485,9 +495,19 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
} }
} }
const query = {
attributes: {
include: attributesInclude
},
offset: options.start,
limit: options.count,
order: getSort(options.sort),
where
}
return VideoChannelModel return VideoChannelModel
.scope({ .scope({
method: [ ScopeNames.FOR_API, { actorId: options.actorId, host: options.host } as AvailableForListOptions ] method: [ ScopeNames.FOR_API, { actorId: options.actorId, host: options.host, names: options.names } as AvailableForListOptions ]
}) })
.findAndCountAll(query) .findAndCountAll(query)
.then(({ rows, count }) => { .then(({ rows, count }) => {

View file

@ -83,6 +83,7 @@ type AvailableForListOptions = {
listMyPlaylists?: boolean listMyPlaylists?: boolean
search?: string search?: string
host?: string host?: string
uuids?: string[]
withVideos?: boolean withVideos?: boolean
} }
@ -200,18 +201,26 @@ function getVideoLengthSelect () {
}) })
} }
if (options.uuids) {
whereAnd.push({
uuid: {
[Op.in]: options.uuids
}
})
}
if (options.withVideos === true) { if (options.withVideos === true) {
whereAnd.push( whereAnd.push(
literal(`(${getVideoLengthSelect()}) != 0`) literal(`(${getVideoLengthSelect()}) != 0`)
) )
} }
const attributesInclude = [] let attributesInclude: any[] = [ literal('0 as similarity') ]
if (options.search) { if (options.search) {
const escapedSearch = VideoPlaylistModel.sequelize.escape(options.search) const escapedSearch = VideoPlaylistModel.sequelize.escape(options.search)
const escapedLikeSearch = VideoPlaylistModel.sequelize.escape('%' + options.search + '%') const escapedLikeSearch = VideoPlaylistModel.sequelize.escape('%' + options.search + '%')
attributesInclude.push(createSimilarityAttribute('VideoPlaylistModel.name', options.search)) attributesInclude = [ createSimilarityAttribute('VideoPlaylistModel.name', options.search) ]
whereAnd.push({ whereAnd.push({
[Op.or]: [ [Op.or]: [
@ -359,6 +368,7 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli
listMyPlaylists?: boolean listMyPlaylists?: boolean
search?: string search?: string
host?: string host?: string
uuids?: string[]
withVideos?: boolean // false by default withVideos?: boolean // false by default
}) { }) {
const query = { const query = {
@ -379,6 +389,7 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli
listMyPlaylists: options.listMyPlaylists, listMyPlaylists: options.listMyPlaylists,
search: options.search, search: options.search,
host: options.host, host: options.host,
uuids: options.uuids,
withVideos: options.withVideos || false withVideos: options.withVideos || false
} as AvailableForListOptions } as AvailableForListOptions
] ]
@ -402,6 +413,7 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli
sort: string sort: string
search?: string search?: string
host?: string host?: string
uuids?: string[]
}) { }) {
return VideoPlaylistModel.listForApi({ return VideoPlaylistModel.listForApi({
...options, ...options,

View file

@ -1132,6 +1132,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
durationMax?: number // seconds durationMax?: number // seconds
user?: MUserAccountId user?: MUserAccountId
filter?: VideoFilter filter?: VideoFilter
uuids?: string[]
}) { }) {
const serverActor = await getServerActor() const serverActor = await getServerActor()
@ -1167,6 +1168,8 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
durationMin: options.durationMin, durationMin: options.durationMin,
durationMax: options.durationMax, durationMax: options.durationMax,
uuids: options.uuids,
search: options.search search: options.search
} }

View file

@ -146,6 +146,16 @@ describe('Test videos API validator', function () {
const customQuery = { ...query, host: 'example.com' } const customQuery = { ...query, host: 'example.com' }
await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 }) await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 })
}) })
it('Should fail with invalid uuids', async function () {
const customQuery = { ...query, uuids: [ '6565', 'dfd70b83-639f-4980-94af-304a56ab4b35' ] }
await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
})
it('Should succeed with valid uuids', async function () {
const customQuery = { ...query, uuids: [ 'dfd70b83-639f-4980-94af-304a56ab4b35' ] }
await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 })
})
}) })
describe('When searching video playlists', function () { describe('When searching video playlists', function () {
@ -172,6 +182,11 @@ describe('Test videos API validator', function () {
await makeGetRequest({ url: server.url, path, query: { ...query, host: '6565' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) await makeGetRequest({ url: server.url, path, query: { ...query, host: '6565' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
}) })
it('Should fail with invalid uuids', async function () {
const customQuery = { ...query, uuids: [ '6565', 'dfd70b83-639f-4980-94af-304a56ab4b35' ] }
await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
})
it('Should succeed with the correct parameters', async function () { it('Should succeed with the correct parameters', async function () {
await makeGetRequest({ url: server.url, path, query, expectedStatus: HttpStatusCode.OK_200 }) await makeGetRequest({ url: server.url, path, query, expectedStatus: HttpStatusCode.OK_200 })
}) })

View file

@ -22,8 +22,12 @@ describe('Test channels search', function () {
before(async function () { before(async function () {
this.timeout(120000) this.timeout(120000)
server = await createSingleServer(1) const servers = await Promise.all([
remoteServer = await createSingleServer(2, { transcoding: { enabled: false } }) createSingleServer(1),
createSingleServer(2, { transcoding: { enabled: false } })
])
server = servers[0]
remoteServer = servers[1]
await setAccessTokensToServers([ server, remoteServer ]) await setAccessTokensToServers([ server, remoteServer ])
@ -116,6 +120,22 @@ describe('Test channels search', function () {
} }
}) })
it('Should filter by names', async function () {
{
const body = await command.advancedChannelSearch({ search: { names: [ 'squall_channel', 'zell_channel' ] } })
expect(body.total).to.equal(2)
expect(body.data).to.have.lengthOf(2)
expect(body.data[0].displayName).to.equal('Squall channel')
expect(body.data[1].displayName).to.equal('Zell channel')
}
{
const body = await command.advancedChannelSearch({ search: { names: [ 'chocobozzz_channel' ] } })
expect(body.total).to.equal(0)
expect(body.data).to.have.lengthOf(0)
}
})
after(async function () { after(async function () {
await cleanupTests([ server ]) await cleanupTests([ server ])
}) })

View file

@ -19,12 +19,18 @@ describe('Test playlists search', function () {
let server: PeerTubeServer let server: PeerTubeServer
let remoteServer: PeerTubeServer let remoteServer: PeerTubeServer
let command: SearchCommand let command: SearchCommand
let playlistUUID: string
let playlistShortUUID: string
before(async function () { before(async function () {
this.timeout(120000) this.timeout(120000)
server = await createSingleServer(1) const servers = await Promise.all([
remoteServer = await createSingleServer(2, { transcoding: { enabled: false } }) createSingleServer(1),
createSingleServer(2, { transcoding: { enabled: false } })
])
server = servers[0]
remoteServer = servers[1]
await setAccessTokensToServers([ remoteServer, server ]) await setAccessTokensToServers([ remoteServer, server ])
await setDefaultVideoChannel([ remoteServer, server ]) await setDefaultVideoChannel([ remoteServer, server ])
@ -38,6 +44,8 @@ describe('Test playlists search', function () {
videoChannelId: server.store.channel.id videoChannelId: server.store.channel.id
} }
const created = await server.playlists.create({ attributes }) const created = await server.playlists.create({ attributes })
playlistUUID = created.uuid
playlistShortUUID = created.shortUUID
await server.playlists.addElement({ playlistId: created.id, attributes: { videoId } }) await server.playlists.addElement({ playlistId: created.id, attributes: { videoId } })
} }
@ -136,6 +144,22 @@ describe('Test playlists search', function () {
} }
}) })
it('Should filter by UUIDs', async function () {
for (const uuid of [ playlistUUID, playlistShortUUID ]) {
const body = await command.advancedPlaylistSearch({ search: { uuids: [ uuid ] } })
expect(body.total).to.equal(1)
expect(body.data[0].displayName).to.equal('Dr. Kenzo Tenma hospital videos')
}
{
const body = await command.advancedPlaylistSearch({ search: { uuids: [ 'dfd70b83-639f-4980-94af-304a56ab4b35' ] } })
expect(body.total).to.equal(0)
expect(body.data).to.have.lengthOf(0)
}
})
it('Should not display playlists without videos', async function () { it('Should not display playlists without videos', async function () {
const search = { const search = {
search: 'Lunge', search: 'Lunge',

View file

@ -22,14 +22,19 @@ describe('Test videos search', function () {
let remoteServer: PeerTubeServer let remoteServer: PeerTubeServer
let startDate: string let startDate: string
let videoUUID: string let videoUUID: string
let videoShortUUID: string
let command: SearchCommand let command: SearchCommand
before(async function () { before(async function () {
this.timeout(120000) this.timeout(120000)
server = await createSingleServer(1) const servers = await Promise.all([
remoteServer = await createSingleServer(2) createSingleServer(1),
createSingleServer(2)
])
server = servers[0]
remoteServer = servers[1]
await setAccessTokensToServers([ server, remoteServer ]) await setAccessTokensToServers([ server, remoteServer ])
await setDefaultVideoChannel([ server, remoteServer ]) await setDefaultVideoChannel([ server, remoteServer ])
@ -50,8 +55,9 @@ describe('Test videos search', function () {
{ {
const attributes3 = { ...attributes1, name: attributes1.name + ' - 3', language: undefined } const attributes3 = { ...attributes1, name: attributes1.name + ' - 3', language: undefined }
const { id, uuid } = await server.videos.upload({ attributes: attributes3 }) const { id, uuid, shortUUID } = await server.videos.upload({ attributes: attributes3 })
videoUUID = uuid videoUUID = uuid
videoShortUUID = shortUUID
await server.captions.add({ await server.captions.add({
language: 'en', language: 'en',
@ -479,6 +485,22 @@ describe('Test videos search', function () {
expect(body.data[0].name).to.equal('1111 2222 3333 - 3') expect(body.data[0].name).to.equal('1111 2222 3333 - 3')
}) })
it('Should filter by UUIDs', async function () {
for (const uuid of [ videoUUID, videoShortUUID ]) {
const body = await command.advancedVideoSearch({ search: { uuids: [ uuid ] } })
expect(body.total).to.equal(1)
expect(body.data[0].name).to.equal('1111 2222 3333 - 3')
}
{
const body = await command.advancedVideoSearch({ search: { uuids: [ 'dfd70b83-639f-4980-94af-304a56ab4b35' ] } })
expect(body.total).to.equal(0)
expect(body.data).to.have.lengthOf(0)
}
})
it('Should search by host', async function () { it('Should search by host', async function () {
{ {
const body = await command.advancedVideoSearch({ search: { search: '6666 7777 8888', host: server.host } }) const body = await command.advancedVideoSearch({ search: { search: '6666 7777 8888', host: server.host } })

View file

@ -1,11 +1,12 @@
import { SearchTargetQuery } from './search-target-query.model' import { SearchTargetQuery } from './search-target-query.model'
export interface VideoChannelsSearchQuery extends SearchTargetQuery { export interface VideoChannelsSearchQuery extends SearchTargetQuery {
search: string search?: string
start?: number start?: number
count?: number count?: number
sort?: string sort?: string
host?: string host?: string
names?: string[]
} }

View file

@ -1,11 +1,12 @@
import { SearchTargetQuery } from './search-target-query.model' import { SearchTargetQuery } from './search-target-query.model'
export interface VideoPlaylistsSearchQuery extends SearchTargetQuery { export interface VideoPlaylistsSearchQuery extends SearchTargetQuery {
search: string search?: string
start?: number start?: number
count?: number count?: number
sort?: string sort?: string
host?: string host?: string
uuids?: string[]
} }

View file

@ -14,4 +14,7 @@ export interface VideosSearchQuery extends SearchTargetQuery, VideosCommonQuery
durationMin?: number // seconds durationMin?: number // seconds
durationMax?: number // seconds durationMax?: number // seconds
// UUIDs or short
uuids?: string[]
} }