Filter host for channels and playlists search

pull/4300/head
Chocobozzz 2021-07-28 10:32:40 +02:00
parent f68d1cb6ac
commit fa47956ecf
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
18 changed files with 237 additions and 80 deletions

View File

@ -98,7 +98,8 @@ async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: expr
search: query.search, search: query.search,
start: query.start, start: query.start,
count: query.count, count: query.count,
sort: query.sort sort: query.sort,
host: query.host
}, '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

@ -88,7 +88,8 @@ async function searchVideoPlaylistsDB (query: VideoPlaylistsSearchQuery, res: ex
search: query.search, search: query.search,
start: query.start, start: query.start,
count: query.count, count: query.count,
sort: query.sort sort: query.sort,
host: query.host
}, '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

@ -1,6 +1,6 @@
import * as retry from 'async/retry' import * as retry from 'async/retry'
import * as Bluebird from 'bluebird' import * as Bluebird from 'bluebird'
import { BindOrReplacements, QueryTypes, Transaction } from 'sequelize' import { Transaction } from 'sequelize'
import { Model } from 'sequelize-typescript' import { Model } from 'sequelize-typescript'
import { sequelizeTypescript } from '@server/initializers/database' import { sequelizeTypescript } from '@server/initializers/database'
import { logger } from './logger' import { logger } from './logger'
@ -95,18 +95,6 @@ function deleteAllModels <T extends Pick<Model, 'destroy'>> (models: T[], transa
return Promise.all(models.map(f => f.destroy({ transaction }))) return Promise.all(models.map(f => f.destroy({ transaction })))
} }
// Sequelize always skip the update if we only update updatedAt field
function setAsUpdated (table: string, id: number, transaction?: Transaction) {
return sequelizeTypescript.query(
`UPDATE "${table}" SET "updatedAt" = :updatedAt WHERE id = :id`,
{
replacements: { table, id, updatedAt: new Date() },
type: QueryTypes.UPDATE,
transaction
}
)
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function runInReadCommittedTransaction <T> (fn: (t: Transaction) => Promise<T>) { function runInReadCommittedTransaction <T> (fn: (t: Transaction) => Promise<T>) {
@ -123,19 +111,6 @@ function afterCommitIfTransaction (t: Transaction, fn: Function) {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function doesExist (query: string, bind?: BindOrReplacements) {
const options = {
type: QueryTypes.SELECT as QueryTypes.SELECT,
bind,
raw: true
}
return sequelizeTypescript.query(query, options)
.then(results => results.length === 1)
}
// ---------------------------------------------------------------------------
export { export {
resetSequelizeInstance, resetSequelizeInstance,
retryTransactionWrapper, retryTransactionWrapper,
@ -144,7 +119,5 @@ export {
afterCommitIfTransaction, afterCommitIfTransaction,
filterNonExistingModels, filterNonExistingModels,
deleteAllModels, deleteAllModels,
setAsUpdated, runInReadCommittedTransaction
runInReadCommittedTransaction,
doesExist
} }

View File

@ -43,7 +43,14 @@ const videosSearchValidator = [
const videoChannelsListSearchValidator = [ const videoChannelsListSearchValidator = [
query('search').not().isEmpty().withMessage('Should have a valid search'), query('search').not().isEmpty().withMessage('Should have a valid search'),
query('searchTarget').optional().custom(isSearchTargetValid).withMessage('Should have a valid search target'),
query('host')
.optional()
.custom(isHostValid).withMessage('Should have a valid host'),
query('searchTarget')
.optional()
.custom(isSearchTargetValid).withMessage('Should have a valid search target'),
(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 })
@ -56,7 +63,14 @@ const videoChannelsListSearchValidator = [
const videoPlaylistsListSearchValidator = [ const videoPlaylistsListSearchValidator = [
query('search').not().isEmpty().withMessage('Should have a valid search'), query('search').not().isEmpty().withMessage('Should have a valid search'),
query('searchTarget').optional().custom(isSearchTargetValid).withMessage('Should have a valid search target'),
query('host')
.optional()
.custom(isHostValid).withMessage('Should have a valid host'),
query('searchTarget')
.optional()
.custom(isSearchTargetValid).withMessage('Should have a valid search target'),
(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

@ -52,6 +52,7 @@ export enum ScopeNames {
export type SummaryOptions = { export type SummaryOptions = {
actorRequired?: boolean // Default: true actorRequired?: boolean // Default: true
whereActor?: WhereOptions whereActor?: WhereOptions
whereServer?: WhereOptions
withAccountBlockerIds?: number[] withAccountBlockerIds?: number[]
} }
@ -65,12 +66,11 @@ export type SummaryOptions = {
})) }))
@Scopes(() => ({ @Scopes(() => ({
[ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => { [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => {
const whereActor = options.whereActor || undefined
const serverInclude: IncludeOptions = { const serverInclude: IncludeOptions = {
attributes: [ 'host' ], attributes: [ 'host' ],
model: ServerModel.unscoped(), model: ServerModel.unscoped(),
required: false required: !!options.whereServer,
where: options.whereServer
} }
const queryInclude: Includeable[] = [ const queryInclude: Includeable[] = [
@ -78,7 +78,7 @@ export type SummaryOptions = {
attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
model: ActorModel.unscoped(), model: ActorModel.unscoped(),
required: options.actorRequired ?? true, required: options.actorRequired ?? true,
where: whereActor, where: options.whereActor,
include: [ include: [
serverInclude, serverInclude,

View File

@ -19,7 +19,6 @@ import {
UpdatedAt UpdatedAt
} from 'sequelize-typescript' } from 'sequelize-typescript'
import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activitypub/misc' import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activitypub/misc'
import { doesExist } from '@server/helpers/database-utils'
import { getServerActor } from '@server/models/application/application' import { getServerActor } from '@server/models/application/application'
import { import {
MActorFollowActorsDefault, MActorFollowActorsDefault,
@ -36,6 +35,7 @@ import { logger } from '../../helpers/logger'
import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants' import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants'
import { AccountModel } from '../account/account' import { AccountModel } from '../account/account'
import { ServerModel } from '../server/server' import { ServerModel } from '../server/server'
import { doesExist } from '../shared/query'
import { createSafeIn, getFollowsSort, getSort, searchAttribute, throwIfNotValid } from '../utils' import { createSafeIn, getFollowsSort, getSort, searchAttribute, throwIfNotValid } from '../utils'
import { VideoChannelModel } from '../video/video-channel' import { VideoChannelModel } from '../video/video-channel'
import { ActorModel, unusedActorAttributesForAPI } from './actor' import { ActorModel, unusedActorAttributesForAPI } from './actor'

View File

@ -0,0 +1,2 @@
export * from './query'
export * from './update'

View File

@ -0,0 +1,17 @@
import { BindOrReplacements, QueryTypes } from 'sequelize'
import { sequelizeTypescript } from '@server/initializers/database'
function doesExist (query: string, bind?: BindOrReplacements) {
const options = {
type: QueryTypes.SELECT as QueryTypes.SELECT,
bind,
raw: true
}
return sequelizeTypescript.query(query, options)
.then(results => results.length === 1)
}
export {
doesExist
}

View File

@ -0,0 +1,18 @@
import { QueryTypes, Transaction } from 'sequelize'
import { sequelizeTypescript } from '@server/initializers/database'
// Sequelize always skip the update if we only update updatedAt field
function setAsUpdated (table: string, id: number, transaction?: Transaction) {
return sequelizeTypescript.query(
`UPDATE "${table}" SET "updatedAt" = :updatedAt WHERE id = :id`,
{
replacements: { table, id, updatedAt: new Date() },
type: QueryTypes.UPDATE,
transaction
}
)
}
export {
setAsUpdated
}

View File

@ -1,4 +1,4 @@
import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions, Transaction } from 'sequelize' import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions, Transaction, WhereOptions } from 'sequelize'
import { import {
AllowNull, AllowNull,
BeforeDestroy, BeforeDestroy,
@ -17,7 +17,6 @@ import {
Table, Table,
UpdatedAt UpdatedAt
} from 'sequelize-typescript' } from 'sequelize-typescript'
import { setAsUpdated } from '@server/helpers/database-utils'
import { MAccountActor } from '@server/types/models' import { MAccountActor } from '@server/types/models'
import { AttributesOnly } from '@shared/core-utils' import { AttributesOnly } from '@shared/core-utils'
import { ActivityPubActor } from '../../../shared/models/activitypub' import { ActivityPubActor } from '../../../shared/models/activitypub'
@ -41,6 +40,7 @@ import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor'
import { ActorFollowModel } from '../actor/actor-follow' import { ActorFollowModel } from '../actor/actor-follow'
import { ActorImageModel } from '../actor/actor-image' import { ActorImageModel } from '../actor/actor-image'
import { ServerModel } from '../server/server' import { ServerModel } from '../server/server'
import { setAsUpdated } from '../shared'
import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils'
import { VideoModel } from './video' import { VideoModel } from './video'
import { VideoPlaylistModel } from './video-playlist' import { VideoPlaylistModel } from './video-playlist'
@ -58,6 +58,7 @@ export enum ScopeNames {
type AvailableForListOptions = { type AvailableForListOptions = {
actorId: number actorId: number
search?: string search?: string
host?: string
} }
type AvailableWithStatsOptions = { type AvailableWithStatsOptions = {
@ -83,6 +84,33 @@ 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 = {
[Op.or]: [
{
serverId: null
},
{
serverId: {
[Op.in]: Sequelize.literal(inQueryInstanceFollow)
}
}
]
}
let serverRequired = false
let whereServer: WhereOptions
if (options.host && options.host !== WEBSERVER.HOST) {
serverRequired = true
whereServer = { host: options.host }
}
if (options.host === WEBSERVER.HOST) {
Object.assign(whereActor, {
[Op.and]: [ { serverId: null } ]
})
}
return { return {
include: [ include: [
{ {
@ -90,19 +118,18 @@ export type SummaryOptions = {
exclude: unusedActorAttributesForAPI exclude: unusedActorAttributesForAPI
}, },
model: ActorModel, model: ActorModel,
where: { where: whereActor,
[Op.or]: [
{
serverId: null
},
{
serverId: {
[Op.in]: Sequelize.literal(inQueryInstanceFollow)
}
}
]
},
include: [ include: [
{
model: ServerModel,
required: serverRequired,
where: whereServer
},
{
model: ActorImageModel,
as: 'Avatar',
required: false
},
{ {
model: ActorImageModel, model: ActorImageModel,
as: 'Banner', as: 'Banner',
@ -431,6 +458,8 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
start: number start: number
count: number count: number
sort: string sort: string
host?: string
}) { }) {
const attributesInclude = [] const attributesInclude = []
const escapedSearch = VideoChannelModel.sequelize.escape(options.search) const escapedSearch = VideoChannelModel.sequelize.escape(options.search)
@ -458,7 +487,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
return VideoChannelModel return VideoChannelModel
.scope({ .scope({
method: [ ScopeNames.FOR_API, { actorId: options.actorId } as AvailableForListOptions ] method: [ ScopeNames.FOR_API, { actorId: options.actorId, host: options.host } as AvailableForListOptions ]
}) })
.findAndCountAll(query) .findAndCountAll(query)
.then(({ rows, count }) => { .then(({ rows, count }) => {

View File

@ -21,7 +21,6 @@ import {
import { Where } from 'sequelize/types/lib/utils' import { Where } from 'sequelize/types/lib/utils'
import validator from 'validator' import validator from 'validator'
import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub' import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
import { doesExist } from '@server/helpers/database-utils'
import { logger } from '@server/helpers/logger' import { logger } from '@server/helpers/logger'
import { extractVideo } from '@server/helpers/video' import { extractVideo } from '@server/helpers/video'
import { getTorrentFilePath } from '@server/lib/video-paths' import { getTorrentFilePath } from '@server/lib/video-paths'
@ -45,6 +44,7 @@ import {
} from '../../initializers/constants' } from '../../initializers/constants'
import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file' import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file'
import { VideoRedundancyModel } from '../redundancy/video-redundancy' import { VideoRedundancyModel } from '../redundancy/video-redundancy'
import { doesExist } from '../shared'
import { parseAggregateResult, throwIfNotValid } from '../utils' import { parseAggregateResult, throwIfNotValid } from '../utils'
import { VideoModel } from './video' import { VideoModel } from './video'
import { VideoStreamingPlaylistModel } from './video-streaming-playlist' import { VideoStreamingPlaylistModel } from './video-streaming-playlist'

View File

@ -17,7 +17,6 @@ import {
Table, Table,
UpdatedAt UpdatedAt
} from 'sequelize-typescript' } from 'sequelize-typescript'
import { setAsUpdated } from '@server/helpers/database-utils'
import { buildUUID, uuidToShort } from '@server/helpers/uuid' import { buildUUID, uuidToShort } from '@server/helpers/uuid'
import { MAccountId, MChannelId } from '@server/types/models' import { MAccountId, MChannelId } from '@server/types/models'
import { AttributesOnly, buildPlaylistEmbedPath, buildPlaylistWatchPath } from '@shared/core-utils' import { AttributesOnly, buildPlaylistEmbedPath, buildPlaylistWatchPath } from '@shared/core-utils'
@ -53,6 +52,7 @@ import {
} from '../../types/models/video/video-playlist' } from '../../types/models/video/video-playlist'
import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account'
import { ActorModel } from '../actor/actor' import { ActorModel } from '../actor/actor'
import { setAsUpdated } from '../shared'
import { import {
buildServerIdsFollowedBy, buildServerIdsFollowedBy,
buildTrigramSearchIndex, buildTrigramSearchIndex,
@ -82,6 +82,7 @@ type AvailableForListOptions = {
videoChannelId?: number videoChannelId?: number
listMyPlaylists?: boolean listMyPlaylists?: boolean
search?: string search?: string
host?: string
withVideos?: boolean withVideos?: boolean
} }
@ -141,9 +142,19 @@ function getVideoLengthSelect () {
] ]
}, },
[ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => { [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => {
const whereAnd: WhereOptions[] = []
const whereServer = options.host && options.host !== WEBSERVER.HOST
? { host: options.host }
: undefined
let whereActor: WhereOptions = {} let whereActor: WhereOptions = {}
const whereAnd: WhereOptions[] = [] if (options.host === WEBSERVER.HOST) {
whereActor = {
[Op.and]: [ { serverId: null } ]
}
}
if (options.listMyPlaylists !== true) { if (options.listMyPlaylists !== true) {
whereAnd.push({ whereAnd.push({
@ -168,9 +179,7 @@ function getVideoLengthSelect () {
}) })
} }
whereActor = { Object.assign(whereActor, { [Op.or]: whereActorOr })
[Op.or]: whereActorOr
}
} }
if (options.accountId) { if (options.accountId) {
@ -228,7 +237,7 @@ function getVideoLengthSelect () {
include: [ include: [
{ {
model: AccountModel.scope({ model: AccountModel.scope({
method: [ AccountScopeNames.SUMMARY, { whereActor } as SummaryOptions ] method: [ AccountScopeNames.SUMMARY, { whereActor, whereServer } as SummaryOptions ]
}), }),
required: true required: true
}, },
@ -349,6 +358,7 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli
videoChannelId?: number videoChannelId?: number
listMyPlaylists?: boolean listMyPlaylists?: boolean
search?: string search?: string
host?: string
withVideos?: boolean // false by default withVideos?: boolean // false by default
}) { }) {
const query = { const query = {
@ -368,6 +378,7 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli
videoChannelId: options.videoChannelId, videoChannelId: options.videoChannelId,
listMyPlaylists: options.listMyPlaylists, listMyPlaylists: options.listMyPlaylists,
search: options.search, search: options.search,
host: options.host,
withVideos: options.withVideos || false withVideos: options.withVideos || false
} as AvailableForListOptions } as AvailableForListOptions
] ]
@ -390,6 +401,7 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli
count: number count: number
sort: string sort: string
search?: string search?: string
host?: string
}) { }) {
return VideoPlaylistModel.listForApi({ return VideoPlaylistModel.listForApi({
...options, ...options,

View File

@ -2,7 +2,6 @@ import * as memoizee from 'memoizee'
import { join } from 'path' import { join } from 'path'
import { Op } from 'sequelize' import { Op } from 'sequelize'
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { doesExist } from '@server/helpers/database-utils'
import { VideoFileModel } from '@server/models/video/video-file' import { VideoFileModel } from '@server/models/video/video-file'
import { MStreamingPlaylist, MVideo } from '@server/types/models' import { MStreamingPlaylist, MVideo } from '@server/types/models'
import { AttributesOnly } from '@shared/core-utils' import { AttributesOnly } from '@shared/core-utils'
@ -20,6 +19,7 @@ import {
WEBSERVER WEBSERVER
} from '../../initializers/constants' } from '../../initializers/constants'
import { VideoRedundancyModel } from '../redundancy/video-redundancy' import { VideoRedundancyModel } from '../redundancy/video-redundancy'
import { doesExist } from '../shared'
import { throwIfNotValid } from '../utils' import { throwIfNotValid } from '../utils'
import { VideoModel } from './video' import { VideoModel } from './video'

View File

@ -24,7 +24,6 @@ import {
Table, Table,
UpdatedAt UpdatedAt
} from 'sequelize-typescript' } from 'sequelize-typescript'
import { setAsUpdated } from '@server/helpers/database-utils'
import { buildNSFWFilter } from '@server/helpers/express-utils' import { buildNSFWFilter } from '@server/helpers/express-utils'
import { uuidToShort } from '@server/helpers/uuid' import { uuidToShort } from '@server/helpers/uuid'
import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video'
@ -92,6 +91,7 @@ import { VideoRedundancyModel } from '../redundancy/video-redundancy'
import { ServerModel } from '../server/server' import { ServerModel } from '../server/server'
import { TrackerModel } from '../server/tracker' import { TrackerModel } from '../server/tracker'
import { VideoTrackerModel } from '../server/video-tracker' import { VideoTrackerModel } from '../server/video-tracker'
import { setAsUpdated } from '../shared'
import { UserModel } from '../user/user' import { UserModel } from '../user/user'
import { UserVideoHistoryModel } from '../user/user-video-history' import { UserVideoHistoryModel } from '../user/user-video-history'
import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils' import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'

View File

@ -2,24 +2,33 @@
import 'mocha' import 'mocha'
import * as chai from 'chai' import * as chai from 'chai'
import { cleanupTests, createSingleServer, PeerTubeServer, SearchCommand, setAccessTokensToServers } from '@shared/extra-utils' import {
cleanupTests,
createSingleServer,
doubleFollow,
PeerTubeServer,
SearchCommand,
setAccessTokensToServers
} from '@shared/extra-utils'
import { VideoChannel } from '@shared/models' import { VideoChannel } from '@shared/models'
const expect = chai.expect const expect = chai.expect
describe('Test channels search', function () { describe('Test channels search', function () {
let server: PeerTubeServer = null let server: PeerTubeServer
let remoteServer: PeerTubeServer
let command: SearchCommand let command: SearchCommand
before(async function () { before(async function () {
this.timeout(30000) this.timeout(30000)
server = await createSingleServer(1) server = await createSingleServer(1)
remoteServer = await createSingleServer(2, { transcoding: { enabled: false } })
await setAccessTokensToServers([ server ]) await setAccessTokensToServers([ server, remoteServer ])
{ {
await server.users.create({ username: 'user1', password: 'password' }) await server.users.create({ username: 'user1' })
const channel = { const channel = {
name: 'squall_channel', name: 'squall_channel',
displayName: 'Squall channel' displayName: 'Squall channel'
@ -27,6 +36,19 @@ describe('Test channels search', function () {
await server.channels.create({ attributes: channel }) await server.channels.create({ attributes: channel })
} }
{
await remoteServer.users.create({ username: 'user1' })
const channel = {
name: 'zell_channel',
displayName: 'Zell channel'
}
const { id } = await remoteServer.channels.create({ attributes: channel })
await remoteServer.videos.upload({ attributes: { channelId: id } })
}
await doubleFollow(server, remoteServer)
command = server.search command = server.search
}) })
@ -66,6 +88,34 @@ describe('Test channels search', function () {
} }
}) })
it('Should filter by host', async function () {
{
const search = { search: 'channel', host: remoteServer.host }
const body = await command.advancedChannelSearch({ search })
expect(body.total).to.equal(1)
expect(body.data).to.have.lengthOf(1)
expect(body.data[0].displayName).to.equal('Zell channel')
}
{
const search = { search: 'Sq', host: server.host }
const body = await command.advancedChannelSearch({ search })
expect(body.total).to.equal(1)
expect(body.data).to.have.lengthOf(1)
expect(body.data[0].displayName).to.equal('Squall channel')
}
{
const search = { search: 'Squall', host: 'example.com' }
const body = await command.advancedChannelSearch({ search })
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

@ -5,6 +5,7 @@ import * as chai from 'chai'
import { import {
cleanupTests, cleanupTests,
createSingleServer, createSingleServer,
doubleFollow,
PeerTubeServer, PeerTubeServer,
SearchCommand, SearchCommand,
setAccessTokensToServers, setAccessTokensToServers,
@ -15,20 +16,22 @@ import { VideoPlaylistPrivacy } from '@shared/models'
const expect = chai.expect const expect = chai.expect
describe('Test playlists search', function () { describe('Test playlists search', function () {
let server: PeerTubeServer = null let server: PeerTubeServer
let remoteServer: PeerTubeServer
let command: SearchCommand let command: SearchCommand
before(async function () { before(async function () {
this.timeout(30000) this.timeout(30000)
server = await createSingleServer(1) server = await createSingleServer(1)
remoteServer = await createSingleServer(2, { transcoding: { enabled: false } })
await setAccessTokensToServers([ server ]) await setAccessTokensToServers([ remoteServer, server ])
await setDefaultVideoChannel([ server ]) await setDefaultVideoChannel([ remoteServer, server ])
const videoId = (await server.videos.quickUpload({ name: 'video' })).uuid
{ {
const videoId = (await server.videos.upload()).uuid
const attributes = { const attributes = {
displayName: 'Dr. Kenzo Tenma hospital videos', displayName: 'Dr. Kenzo Tenma hospital videos',
privacy: VideoPlaylistPrivacy.PUBLIC, privacy: VideoPlaylistPrivacy.PUBLIC,
@ -40,14 +43,16 @@ describe('Test playlists search', function () {
} }
{ {
const attributes = { const videoId = (await remoteServer.videos.upload()).uuid
displayName: 'Johan & Anna Libert musics',
privacy: VideoPlaylistPrivacy.PUBLIC,
videoChannelId: server.store.channel.id
}
const created = await server.playlists.create({ attributes })
await server.playlists.addElement({ playlistId: created.id, attributes: { videoId } }) const attributes = {
displayName: 'Johan & Anna Libert music videos',
privacy: VideoPlaylistPrivacy.PUBLIC,
videoChannelId: remoteServer.store.channel.id
}
const created = await remoteServer.playlists.create({ attributes })
await remoteServer.playlists.addElement({ playlistId: created.id, attributes: { videoId } })
} }
{ {
@ -59,6 +64,8 @@ describe('Test playlists search', function () {
await server.playlists.create({ attributes }) await server.playlists.create({ attributes })
} }
await doubleFollow(server, remoteServer)
command = server.search command = server.search
}) })
@ -87,7 +94,7 @@ describe('Test playlists search', function () {
{ {
const search = { const search = {
search: 'Anna Livert', search: 'Anna Livert music',
start: 0, start: 0,
count: 1 count: 1
} }
@ -96,7 +103,36 @@ describe('Test playlists search', function () {
expect(body.data).to.have.lengthOf(1) expect(body.data).to.have.lengthOf(1)
const playlist = body.data[0] const playlist = body.data[0]
expect(playlist.displayName).to.equal('Johan & Anna Libert musics') expect(playlist.displayName).to.equal('Johan & Anna Libert music videos')
}
})
it('Should filter by host', async function () {
{
const search = { search: 'tenma', host: server.host }
const body = await command.advancedPlaylistSearch({ search })
expect(body.total).to.equal(1)
expect(body.data).to.have.lengthOf(1)
const playlist = body.data[0]
expect(playlist.displayName).to.equal('Dr. Kenzo Tenma hospital videos')
}
{
const search = { search: 'Anna', host: 'example.com' }
const body = await command.advancedPlaylistSearch({ search })
expect(body.total).to.equal(0)
expect(body.data).to.have.lengthOf(0)
}
{
const search = { search: 'video', host: remoteServer.host }
const body = await command.advancedPlaylistSearch({ search })
expect(body.total).to.equal(1)
expect(body.data).to.have.lengthOf(1)
const playlist = body.data[0]
expect(playlist.displayName).to.equal('Johan & Anna Libert music videos')
} }
}) })

View File

@ -6,4 +6,6 @@ export interface VideoChannelsSearchQuery extends SearchTargetQuery {
start?: number start?: number
count?: number count?: number
sort?: string sort?: string
host?: string
} }

View File

@ -6,4 +6,6 @@ export interface VideoPlaylistsSearchQuery extends SearchTargetQuery {
start?: number start?: number
count?: number count?: number
sort?: string sort?: string
host?: string
} }