function searchVideoChannels (req: express.Request, res: express.Response) {
const query: VideoChannelsSearchQuery = req.query
- const search = query.search
+ let search = query.search || ''
const parts = search.split('@')
if (isURISearch(search) || isWebfingerSearch) return searchVideoChannelURI(search, isWebfingerSearch, res)
// @username -> username to search in DB
- if (query.search.startsWith('@')) query.search = query.search.replace(/^@/, '')
+ if (search.startsWith('@')) search = search.replace(/^@/, '')
if (isSearchIndexSearch(query)) {
return searchVideoChannelsIndex(query, res)
start: query.start,
count: query.count,
sort: query.sort,
- host: query.host
+ host: query.host,
+ names: query.names
}, 'filter:api.search.video-channels.local.list.params')
const resultList = await Hooks.wrapPromiseFun(
start: query.start,
count: query.count,
sort: query.sort,
- host: query.host
+ host: query.host,
+ uuids: query.uuids
}, 'filter:api.search.video-playlists.local.list.params')
const resultList = await Hooks.wrapPromiseFun(
return exists(value) && validator.isUUID('' + value, 4)
}
+function areUUIDsValid (values: string[]) {
+ return isArray(values) && values.every(v => isUUIDValid(v))
+}
+
function isIdOrUUIDValid (value: string) {
return isIdValid(value) || isUUIDValid(value)
}
return value
}
+function toCompleteUUIDs (values: string[]) {
+ return values.map(v => toCompleteUUID(v))
+}
+
function toIntOrNull (value: string) {
const v = toValueOrNull(value)
isIdValid,
isSafePath,
isUUIDValid,
+ toCompleteUUIDs,
toCompleteUUID,
isIdOrUUIDValid,
isDateValid,
toBooleanOrNull,
isBooleanValid,
toIntOrNull,
+ areUUIDsValid,
toArray,
toIntArray,
isFileFieldValid,
import { query } from 'express-validator'
import { isSearchTargetValid } from '@server/helpers/custom-validators/search'
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 { areValidationErrors } from './shared'
.optional()
.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('durationMax').optional().isInt().withMessage('Should have a valid max duration'),
+ query('durationMin')
+ .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'),
]
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')
.optional()
.optional()
.custom(isSearchTargetValid).withMessage('Should have a valid search target'),
+ query('names')
+ .optional()
+ .toArray(),
+
(req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking video channels search query', { parameters: req.query })
]
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')
.optional()
.optional()
.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) => {
logger.debug('Checking video playlists search query', { parameters: req.query })
tagsOneOf?: string[]
tagsAllOf?: string[]
+ uuids?: string[]
+
withFiles?: boolean
accountId?: number
this.whereTagsAllOf(options.tagsAllOf)
}
+ if (options.uuids) {
+ this.whereUUIDs(options.uuids)
+ }
+
if (options.nsfw === true) {
this.whereNSFW()
} else if (options.nsfw === false) {
)
}
+ private whereUUIDs (uuids: string[]) {
+ this.and.push('"video"."uuid" IN (' + createSafeIn(this.sequelize, uuids) + ')')
+ }
+
private whereCategoryOneOf (categoryOneOf: number[]) {
this.and.push('"video"."category" IN (:categoryOneOf)')
this.replacements.categoryOneOf = categoryOneOf
actorId: number
search?: string
host?: string
+ names?: string[]
}
type AvailableWithStatsOptions = {
// Only list local channels OR channels that are on an instance followed by actorId
const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId)
- const whereActor = {
- [Op.or]: [
- {
- serverId: null
- },
- {
- serverId: {
- [Op.in]: Sequelize.literal(inQueryInstanceFollow)
+ const whereActorAnd: WhereOptions[] = [
+ {
+ [Op.or]: [
+ {
+ serverId: null
+ },
+ {
+ serverId: {
+ [Op.in]: Sequelize.literal(inQueryInstanceFollow)
+ }
}
- }
- ]
- }
+ ]
+ }
+ ]
let serverRequired = false
let whereServer: WhereOptions
}
if (options.host === WEBSERVER.HOST) {
- Object.assign(whereActor, {
- [Op.and]: [ { serverId: null } ]
+ whereActorAnd.push({
+ serverId: null
+ })
+ }
+
+ if (options.names) {
+ whereActorAnd.push({
+ preferredUsername: {
+ [Op.in]: options.names
+ }
})
}
exclude: unusedActorAttributesForAPI
},
model: ActorModel,
- where: whereActor,
+ where: {
+ [Op.and]: whereActorAnd
+ },
include: [
{
model: ServerModel,
static searchForApi (options: {
actorId: number
- search: string
+ search?: string
start: number
count: number
sort: string
host?: string
+ names?: string[]
}) {
- const attributesInclude = []
- const escapedSearch = VideoChannelModel.sequelize.escape(options.search)
- const escapedLikeSearch = VideoChannelModel.sequelize.escape('%' + options.search + '%')
- attributesInclude.push(createSimilarityAttribute('VideoChannelModel.name', options.search))
+ let attributesInclude: any[] = [ literal('0 as similarity') ]
+ let where: WhereOptions
- const query = {
- attributes: {
- include: attributesInclude
- },
- offset: options.start,
- limit: options.count,
- order: getSort(options.sort),
- where: {
+ if (options.search) {
+ const escapedSearch = VideoChannelModel.sequelize.escape(options.search)
+ const escapedLikeSearch = VideoChannelModel.sequelize.escape('%' + options.search + '%')
+ attributesInclude = [ createSimilarityAttribute('VideoChannelModel.name', options.search) ]
+
+ where = {
[Op.or]: [
Sequelize.literal(
'lower(immutable_unaccent("VideoChannelModel"."name")) % lower(immutable_unaccent(' + escapedSearch + '))'
}
}
+ const query = {
+ attributes: {
+ include: attributesInclude
+ },
+ offset: options.start,
+ limit: options.count,
+ order: getSort(options.sort),
+ where
+ }
+
return VideoChannelModel
.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)
.then(({ rows, count }) => {
listMyPlaylists?: boolean
search?: string
host?: string
+ uuids?: string[]
withVideos?: boolean
}
})
}
+ if (options.uuids) {
+ whereAnd.push({
+ uuid: {
+ [Op.in]: options.uuids
+ }
+ })
+ }
+
if (options.withVideos === true) {
whereAnd.push(
literal(`(${getVideoLengthSelect()}) != 0`)
)
}
- const attributesInclude = []
+ let attributesInclude: any[] = [ literal('0 as similarity') ]
if (options.search) {
const escapedSearch = 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({
[Op.or]: [
listMyPlaylists?: boolean
search?: string
host?: string
+ uuids?: string[]
withVideos?: boolean // false by default
}) {
const query = {
listMyPlaylists: options.listMyPlaylists,
search: options.search,
host: options.host,
+ uuids: options.uuids,
withVideos: options.withVideos || false
} as AvailableForListOptions
]
sort: string
search?: string
host?: string
+ uuids?: string[]
}) {
return VideoPlaylistModel.listForApi({
...options,
durationMax?: number // seconds
user?: MUserAccountId
filter?: VideoFilter
+ uuids?: string[]
}) {
const serverActor = await getServerActor()
durationMin: options.durationMin,
durationMax: options.durationMax,
+ uuids: options.uuids,
+
search: options.search
}
const customQuery = { ...query, host: 'example.com' }
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 () {
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 () {
await makeGetRequest({ url: server.url, path, query, expectedStatus: HttpStatusCode.OK_200 })
})
before(async function () {
this.timeout(120000)
- server = await createSingleServer(1)
- remoteServer = await createSingleServer(2, { transcoding: { enabled: false } })
+ const servers = await Promise.all([
+ createSingleServer(1),
+ createSingleServer(2, { transcoding: { enabled: false } })
+ ])
+ server = servers[0]
+ remoteServer = servers[1]
await setAccessTokensToServers([ server, remoteServer ])
}
})
+ 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 () {
await cleanupTests([ server ])
})
let server: PeerTubeServer
let remoteServer: PeerTubeServer
let command: SearchCommand
+ let playlistUUID: string
+ let playlistShortUUID: string
before(async function () {
this.timeout(120000)
- server = await createSingleServer(1)
- remoteServer = await createSingleServer(2, { transcoding: { enabled: false } })
+ const servers = await Promise.all([
+ createSingleServer(1),
+ createSingleServer(2, { transcoding: { enabled: false } })
+ ])
+ server = servers[0]
+ remoteServer = servers[1]
await setAccessTokensToServers([ remoteServer, server ])
await setDefaultVideoChannel([ remoteServer, server ])
videoChannelId: server.store.channel.id
}
const created = await server.playlists.create({ attributes })
+ playlistUUID = created.uuid
+ playlistShortUUID = created.shortUUID
await server.playlists.addElement({ playlistId: created.id, attributes: { videoId } })
}
}
})
+ 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 () {
const search = {
search: 'Lunge',
let remoteServer: PeerTubeServer
let startDate: string
let videoUUID: string
+ let videoShortUUID: string
let command: SearchCommand
before(async function () {
this.timeout(120000)
- server = await createSingleServer(1)
- remoteServer = await createSingleServer(2)
+ const servers = await Promise.all([
+ createSingleServer(1),
+ createSingleServer(2)
+ ])
+ server = servers[0]
+ remoteServer = servers[1]
await setAccessTokensToServers([ server, remoteServer ])
await setDefaultVideoChannel([ server, remoteServer ])
{
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
+ videoShortUUID = shortUUID
await server.captions.add({
language: 'en',
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 () {
{
const body = await command.advancedVideoSearch({ search: { search: '6666 7777 8888', host: server.host } })
import { SearchTargetQuery } from './search-target-query.model'
export interface VideoChannelsSearchQuery extends SearchTargetQuery {
- search: string
+ search?: string
start?: number
count?: number
sort?: string
host?: string
+ names?: string[]
}
import { SearchTargetQuery } from './search-target-query.model'
export interface VideoPlaylistsSearchQuery extends SearchTargetQuery {
- search: string
+ search?: string
start?: number
count?: number
sort?: string
host?: string
+ uuids?: string[]
}
durationMin?: number // seconds
durationMax?: number // seconds
+
+ // UUIDs or short
+ uuids?: string[]
}