aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--server/controllers/api/search/search-video-channels.ts3
-rw-r--r--server/controllers/api/search/search-video-playlists.ts3
-rw-r--r--server/helpers/database-utils.ts31
-rw-r--r--server/middlewares/validators/search.ts18
-rw-r--r--server/models/account/account.ts8
-rw-r--r--server/models/actor/actor-follow.ts2
-rw-r--r--server/models/shared/index.ts2
-rw-r--r--server/models/shared/query.ts17
-rw-r--r--server/models/shared/update.ts18
-rw-r--r--server/models/video/video-channel.ts59
-rw-r--r--server/models/video/video-file.ts2
-rw-r--r--server/models/video/video-playlist.ts24
-rw-r--r--server/models/video/video-streaming-playlist.ts2
-rw-r--r--server/models/video/video.ts2
-rw-r--r--server/tests/api/search/search-channels.ts58
-rw-r--r--server/tests/api/search/search-playlists.ts58
-rw-r--r--shared/models/search/video-channels-search-query.model.ts2
-rw-r--r--shared/models/search/video-playlists-search-query.model.ts2
18 files changed, 234 insertions, 77 deletions
diff --git a/server/controllers/api/search/search-video-channels.ts b/server/controllers/api/search/search-video-channels.ts
index c8f0a0a0b..be0b6b9a2 100644
--- a/server/controllers/api/search/search-video-channels.ts
+++ b/server/controllers/api/search/search-video-channels.ts
@@ -98,7 +98,8 @@ async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: expr
98 search: query.search, 98 search: query.search,
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 }, 'filter:api.search.video-channels.local.list.params') 103 }, 'filter:api.search.video-channels.local.list.params')
103 104
104 const resultList = await Hooks.wrapPromiseFun( 105 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 f55b1fba3..60d1a44f7 100644
--- a/server/controllers/api/search/search-video-playlists.ts
+++ b/server/controllers/api/search/search-video-playlists.ts
@@ -88,7 +88,8 @@ async function searchVideoPlaylistsDB (query: VideoPlaylistsSearchQuery, res: ex
88 search: query.search, 88 search: query.search,
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 }, 'filter:api.search.video-playlists.local.list.params') 93 }, 'filter:api.search.video-playlists.local.list.params')
93 94
94 const resultList = await Hooks.wrapPromiseFun( 95 const resultList = await Hooks.wrapPromiseFun(
diff --git a/server/helpers/database-utils.ts b/server/helpers/database-utils.ts
index 422774022..ec35295df 100644
--- a/server/helpers/database-utils.ts
+++ b/server/helpers/database-utils.ts
@@ -1,6 +1,6 @@
1import * as retry from 'async/retry' 1import * as retry from 'async/retry'
2import * as Bluebird from 'bluebird' 2import * as Bluebird from 'bluebird'
3import { BindOrReplacements, QueryTypes, Transaction } from 'sequelize' 3import { Transaction } from 'sequelize'
4import { Model } from 'sequelize-typescript' 4import { Model } from 'sequelize-typescript'
5import { sequelizeTypescript } from '@server/initializers/database' 5import { sequelizeTypescript } from '@server/initializers/database'
6import { logger } from './logger' 6import { logger } from './logger'
@@ -95,18 +95,6 @@ function deleteAllModels <T extends Pick<Model, 'destroy'>> (models: T[], transa
95 return Promise.all(models.map(f => f.destroy({ transaction }))) 95 return Promise.all(models.map(f => f.destroy({ transaction })))
96} 96}
97 97
98// Sequelize always skip the update if we only update updatedAt field
99function setAsUpdated (table: string, id: number, transaction?: Transaction) {
100 return sequelizeTypescript.query(
101 `UPDATE "${table}" SET "updatedAt" = :updatedAt WHERE id = :id`,
102 {
103 replacements: { table, id, updatedAt: new Date() },
104 type: QueryTypes.UPDATE,
105 transaction
106 }
107 )
108}
109
110// --------------------------------------------------------------------------- 98// ---------------------------------------------------------------------------
111 99
112function runInReadCommittedTransaction <T> (fn: (t: Transaction) => Promise<T>) { 100function runInReadCommittedTransaction <T> (fn: (t: Transaction) => Promise<T>) {
@@ -123,19 +111,6 @@ function afterCommitIfTransaction (t: Transaction, fn: Function) {
123 111
124// --------------------------------------------------------------------------- 112// ---------------------------------------------------------------------------
125 113
126function doesExist (query: string, bind?: BindOrReplacements) {
127 const options = {
128 type: QueryTypes.SELECT as QueryTypes.SELECT,
129 bind,
130 raw: true
131 }
132
133 return sequelizeTypescript.query(query, options)
134 .then(results => results.length === 1)
135}
136
137// ---------------------------------------------------------------------------
138
139export { 114export {
140 resetSequelizeInstance, 115 resetSequelizeInstance,
141 retryTransactionWrapper, 116 retryTransactionWrapper,
@@ -144,7 +119,5 @@ export {
144 afterCommitIfTransaction, 119 afterCommitIfTransaction,
145 filterNonExistingModels, 120 filterNonExistingModels,
146 deleteAllModels, 121 deleteAllModels,
147 setAsUpdated, 122 runInReadCommittedTransaction
148 runInReadCommittedTransaction,
149 doesExist
150} 123}
diff --git a/server/middlewares/validators/search.ts b/server/middlewares/validators/search.ts
index 6bb335127..ea6a490b2 100644
--- a/server/middlewares/validators/search.ts
+++ b/server/middlewares/validators/search.ts
@@ -43,7 +43,14 @@ const videosSearchValidator = [
43 43
44const videoChannelsListSearchValidator = [ 44const videoChannelsListSearchValidator = [
45 query('search').not().isEmpty().withMessage('Should have a valid search'), 45 query('search').not().isEmpty().withMessage('Should have a valid search'),
46 query('searchTarget').optional().custom(isSearchTargetValid).withMessage('Should have a valid search target'), 46
47 query('host')
48 .optional()
49 .custom(isHostValid).withMessage('Should have a valid host'),
50
51 query('searchTarget')
52 .optional()
53 .custom(isSearchTargetValid).withMessage('Should have a valid search target'),
47 54
48 (req: express.Request, res: express.Response, next: express.NextFunction) => { 55 (req: express.Request, res: express.Response, next: express.NextFunction) => {
49 logger.debug('Checking video channels search query', { parameters: req.query }) 56 logger.debug('Checking video channels search query', { parameters: req.query })
@@ -56,7 +63,14 @@ const videoChannelsListSearchValidator = [
56 63
57const videoPlaylistsListSearchValidator = [ 64const videoPlaylistsListSearchValidator = [
58 query('search').not().isEmpty().withMessage('Should have a valid search'), 65 query('search').not().isEmpty().withMessage('Should have a valid search'),
59 query('searchTarget').optional().custom(isSearchTargetValid).withMessage('Should have a valid search target'), 66
67 query('host')
68 .optional()
69 .custom(isHostValid).withMessage('Should have a valid host'),
70
71 query('searchTarget')
72 .optional()
73 .custom(isSearchTargetValid).withMessage('Should have a valid search target'),
60 74
61 (req: express.Request, res: express.Response, next: express.NextFunction) => { 75 (req: express.Request, res: express.Response, next: express.NextFunction) => {
62 logger.debug('Checking video playlists search query', { parameters: req.query }) 76 logger.debug('Checking video playlists search query', { parameters: req.query })
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index 665ecd595..37194a119 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -52,6 +52,7 @@ export enum ScopeNames {
52export type SummaryOptions = { 52export type SummaryOptions = {
53 actorRequired?: boolean // Default: true 53 actorRequired?: boolean // Default: true
54 whereActor?: WhereOptions 54 whereActor?: WhereOptions
55 whereServer?: WhereOptions
55 withAccountBlockerIds?: number[] 56 withAccountBlockerIds?: number[]
56} 57}
57 58
@@ -65,12 +66,11 @@ export type SummaryOptions = {
65})) 66}))
66@Scopes(() => ({ 67@Scopes(() => ({
67 [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => { 68 [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => {
68 const whereActor = options.whereActor || undefined
69
70 const serverInclude: IncludeOptions = { 69 const serverInclude: IncludeOptions = {
71 attributes: [ 'host' ], 70 attributes: [ 'host' ],
72 model: ServerModel.unscoped(), 71 model: ServerModel.unscoped(),
73 required: false 72 required: !!options.whereServer,
73 where: options.whereServer
74 } 74 }
75 75
76 const queryInclude: Includeable[] = [ 76 const queryInclude: Includeable[] = [
@@ -78,7 +78,7 @@ export type SummaryOptions = {
78 attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ], 78 attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
79 model: ActorModel.unscoped(), 79 model: ActorModel.unscoped(),
80 required: options.actorRequired ?? true, 80 required: options.actorRequired ?? true,
81 where: whereActor, 81 where: options.whereActor,
82 include: [ 82 include: [
83 serverInclude, 83 serverInclude,
84 84
diff --git a/server/models/actor/actor-follow.ts b/server/models/actor/actor-follow.ts
index 3080e02a6..283856d3f 100644
--- a/server/models/actor/actor-follow.ts
+++ b/server/models/actor/actor-follow.ts
@@ -19,7 +19,6 @@ import {
19 UpdatedAt 19 UpdatedAt
20} from 'sequelize-typescript' 20} from 'sequelize-typescript'
21import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activitypub/misc' 21import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activitypub/misc'
22import { doesExist } from '@server/helpers/database-utils'
23import { getServerActor } from '@server/models/application/application' 22import { getServerActor } from '@server/models/application/application'
24import { 23import {
25 MActorFollowActorsDefault, 24 MActorFollowActorsDefault,
@@ -36,6 +35,7 @@ import { logger } from '../../helpers/logger'
36import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants' 35import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants'
37import { AccountModel } from '../account/account' 36import { AccountModel } from '../account/account'
38import { ServerModel } from '../server/server' 37import { ServerModel } from '../server/server'
38import { doesExist } from '../shared/query'
39import { createSafeIn, getFollowsSort, getSort, searchAttribute, throwIfNotValid } from '../utils' 39import { createSafeIn, getFollowsSort, getSort, searchAttribute, throwIfNotValid } from '../utils'
40import { VideoChannelModel } from '../video/video-channel' 40import { VideoChannelModel } from '../video/video-channel'
41import { ActorModel, unusedActorAttributesForAPI } from './actor' 41import { ActorModel, unusedActorAttributesForAPI } from './actor'
diff --git a/server/models/shared/index.ts b/server/models/shared/index.ts
new file mode 100644
index 000000000..5b97510e0
--- /dev/null
+++ b/server/models/shared/index.ts
@@ -0,0 +1,2 @@
1export * from './query'
2export * from './update'
diff --git a/server/models/shared/query.ts b/server/models/shared/query.ts
new file mode 100644
index 000000000..036cc13c6
--- /dev/null
+++ b/server/models/shared/query.ts
@@ -0,0 +1,17 @@
1import { BindOrReplacements, QueryTypes } from 'sequelize'
2import { sequelizeTypescript } from '@server/initializers/database'
3
4function doesExist (query: string, bind?: BindOrReplacements) {
5 const options = {
6 type: QueryTypes.SELECT as QueryTypes.SELECT,
7 bind,
8 raw: true
9 }
10
11 return sequelizeTypescript.query(query, options)
12 .then(results => results.length === 1)
13}
14
15export {
16 doesExist
17}
diff --git a/server/models/shared/update.ts b/server/models/shared/update.ts
new file mode 100644
index 000000000..d338211e3
--- /dev/null
+++ b/server/models/shared/update.ts
@@ -0,0 +1,18 @@
1import { QueryTypes, Transaction } from 'sequelize'
2import { sequelizeTypescript } from '@server/initializers/database'
3
4// Sequelize always skip the update if we only update updatedAt field
5function setAsUpdated (table: string, id: number, transaction?: Transaction) {
6 return sequelizeTypescript.query(
7 `UPDATE "${table}" SET "updatedAt" = :updatedAt WHERE id = :id`,
8 {
9 replacements: { table, id, updatedAt: new Date() },
10 type: QueryTypes.UPDATE,
11 transaction
12 }
13 )
14}
15
16export {
17 setAsUpdated
18}
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index 183e7448c..9aa271711 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -1,4 +1,4 @@
1import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions, Transaction } from 'sequelize' 1import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions, Transaction, WhereOptions } from 'sequelize'
2import { 2import {
3 AllowNull, 3 AllowNull,
4 BeforeDestroy, 4 BeforeDestroy,
@@ -17,7 +17,6 @@ import {
17 Table, 17 Table,
18 UpdatedAt 18 UpdatedAt
19} from 'sequelize-typescript' 19} from 'sequelize-typescript'
20import { setAsUpdated } from '@server/helpers/database-utils'
21import { MAccountActor } from '@server/types/models' 20import { MAccountActor } from '@server/types/models'
22import { AttributesOnly } from '@shared/core-utils' 21import { AttributesOnly } from '@shared/core-utils'
23import { ActivityPubActor } from '../../../shared/models/activitypub' 22import { ActivityPubActor } from '../../../shared/models/activitypub'
@@ -41,6 +40,7 @@ import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor'
41import { ActorFollowModel } from '../actor/actor-follow' 40import { ActorFollowModel } from '../actor/actor-follow'
42import { ActorImageModel } from '../actor/actor-image' 41import { ActorImageModel } from '../actor/actor-image'
43import { ServerModel } from '../server/server' 42import { ServerModel } from '../server/server'
43import { setAsUpdated } from '../shared'
44import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' 44import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils'
45import { VideoModel } from './video' 45import { VideoModel } from './video'
46import { VideoPlaylistModel } from './video-playlist' 46import { VideoPlaylistModel } from './video-playlist'
@@ -58,6 +58,7 @@ export enum ScopeNames {
58type AvailableForListOptions = { 58type AvailableForListOptions = {
59 actorId: number 59 actorId: number
60 search?: string 60 search?: string
61 host?: string
61} 62}
62 63
63type AvailableWithStatsOptions = { 64type AvailableWithStatsOptions = {
@@ -83,6 +84,33 @@ export type SummaryOptions = {
83 // Only list local channels OR channels that are on an instance followed by actorId 84 // Only list local channels OR channels that are on an instance followed by actorId
84 const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId) 85 const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId)
85 86
87 const whereActor = {
88 [Op.or]: [
89 {
90 serverId: null
91 },
92 {
93 serverId: {
94 [Op.in]: Sequelize.literal(inQueryInstanceFollow)
95 }
96 }
97 ]
98 }
99
100 let serverRequired = false
101 let whereServer: WhereOptions
102
103 if (options.host && options.host !== WEBSERVER.HOST) {
104 serverRequired = true
105 whereServer = { host: options.host }
106 }
107
108 if (options.host === WEBSERVER.HOST) {
109 Object.assign(whereActor, {
110 [Op.and]: [ { serverId: null } ]
111 })
112 }
113
86 return { 114 return {
87 include: [ 115 include: [
88 { 116 {
@@ -90,20 +118,19 @@ export type SummaryOptions = {
90 exclude: unusedActorAttributesForAPI 118 exclude: unusedActorAttributesForAPI
91 }, 119 },
92 model: ActorModel, 120 model: ActorModel,
93 where: { 121 where: whereActor,
94 [Op.or]: [
95 {
96 serverId: null
97 },
98 {
99 serverId: {
100 [Op.in]: Sequelize.literal(inQueryInstanceFollow)
101 }
102 }
103 ]
104 },
105 include: [ 122 include: [
106 { 123 {
124 model: ServerModel,
125 required: serverRequired,
126 where: whereServer
127 },
128 {
129 model: ActorImageModel,
130 as: 'Avatar',
131 required: false
132 },
133 {
107 model: ActorImageModel, 134 model: ActorImageModel,
108 as: 'Banner', 135 as: 'Banner',
109 required: false 136 required: false
@@ -431,6 +458,8 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
431 start: number 458 start: number
432 count: number 459 count: number
433 sort: string 460 sort: string
461
462 host?: string
434 }) { 463 }) {
435 const attributesInclude = [] 464 const attributesInclude = []
436 const escapedSearch = VideoChannelModel.sequelize.escape(options.search) 465 const escapedSearch = VideoChannelModel.sequelize.escape(options.search)
@@ -458,7 +487,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
458 487
459 return VideoChannelModel 488 return VideoChannelModel
460 .scope({ 489 .scope({
461 method: [ ScopeNames.FOR_API, { actorId: options.actorId } as AvailableForListOptions ] 490 method: [ ScopeNames.FOR_API, { actorId: options.actorId, host: options.host } as AvailableForListOptions ]
462 }) 491 })
463 .findAndCountAll(query) 492 .findAndCountAll(query)
464 .then(({ rows, count }) => { 493 .then(({ rows, count }) => {
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts
index 797a85a4e..09fc5288b 100644
--- a/server/models/video/video-file.ts
+++ b/server/models/video/video-file.ts
@@ -21,7 +21,6 @@ import {
21import { Where } from 'sequelize/types/lib/utils' 21import { Where } from 'sequelize/types/lib/utils'
22import validator from 'validator' 22import validator from 'validator'
23import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub' 23import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
24import { doesExist } from '@server/helpers/database-utils'
25import { logger } from '@server/helpers/logger' 24import { logger } from '@server/helpers/logger'
26import { extractVideo } from '@server/helpers/video' 25import { extractVideo } from '@server/helpers/video'
27import { getTorrentFilePath } from '@server/lib/video-paths' 26import { getTorrentFilePath } from '@server/lib/video-paths'
@@ -45,6 +44,7 @@ import {
45} from '../../initializers/constants' 44} from '../../initializers/constants'
46import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file' 45import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file'
47import { VideoRedundancyModel } from '../redundancy/video-redundancy' 46import { VideoRedundancyModel } from '../redundancy/video-redundancy'
47import { doesExist } from '../shared'
48import { parseAggregateResult, throwIfNotValid } from '../utils' 48import { parseAggregateResult, throwIfNotValid } from '../utils'
49import { VideoModel } from './video' 49import { VideoModel } from './video'
50import { VideoStreamingPlaylistModel } from './video-streaming-playlist' 50import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts
index 72ba474b4..a2dc7075d 100644
--- a/server/models/video/video-playlist.ts
+++ b/server/models/video/video-playlist.ts
@@ -17,7 +17,6 @@ import {
17 Table, 17 Table,
18 UpdatedAt 18 UpdatedAt
19} from 'sequelize-typescript' 19} from 'sequelize-typescript'
20import { setAsUpdated } from '@server/helpers/database-utils'
21import { buildUUID, uuidToShort } from '@server/helpers/uuid' 20import { buildUUID, uuidToShort } from '@server/helpers/uuid'
22import { MAccountId, MChannelId } from '@server/types/models' 21import { MAccountId, MChannelId } from '@server/types/models'
23import { AttributesOnly, buildPlaylistEmbedPath, buildPlaylistWatchPath } from '@shared/core-utils' 22import { AttributesOnly, buildPlaylistEmbedPath, buildPlaylistWatchPath } from '@shared/core-utils'
@@ -53,6 +52,7 @@ import {
53} from '../../types/models/video/video-playlist' 52} from '../../types/models/video/video-playlist'
54import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' 53import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account'
55import { ActorModel } from '../actor/actor' 54import { ActorModel } from '../actor/actor'
55import { setAsUpdated } from '../shared'
56import { 56import {
57 buildServerIdsFollowedBy, 57 buildServerIdsFollowedBy,
58 buildTrigramSearchIndex, 58 buildTrigramSearchIndex,
@@ -82,6 +82,7 @@ type AvailableForListOptions = {
82 videoChannelId?: number 82 videoChannelId?: number
83 listMyPlaylists?: boolean 83 listMyPlaylists?: boolean
84 search?: string 84 search?: string
85 host?: string
85 withVideos?: boolean 86 withVideos?: boolean
86} 87}
87 88
@@ -141,9 +142,19 @@ function getVideoLengthSelect () {
141 ] 142 ]
142 }, 143 },
143 [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => { 144 [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => {
145 const whereAnd: WhereOptions[] = []
146
147 const whereServer = options.host && options.host !== WEBSERVER.HOST
148 ? { host: options.host }
149 : undefined
150
144 let whereActor: WhereOptions = {} 151 let whereActor: WhereOptions = {}
145 152
146 const whereAnd: WhereOptions[] = [] 153 if (options.host === WEBSERVER.HOST) {
154 whereActor = {
155 [Op.and]: [ { serverId: null } ]
156 }
157 }
147 158
148 if (options.listMyPlaylists !== true) { 159 if (options.listMyPlaylists !== true) {
149 whereAnd.push({ 160 whereAnd.push({
@@ -168,9 +179,7 @@ function getVideoLengthSelect () {
168 }) 179 })
169 } 180 }
170 181
171 whereActor = { 182 Object.assign(whereActor, { [Op.or]: whereActorOr })
172 [Op.or]: whereActorOr
173 }
174 } 183 }
175 184
176 if (options.accountId) { 185 if (options.accountId) {
@@ -228,7 +237,7 @@ function getVideoLengthSelect () {
228 include: [ 237 include: [
229 { 238 {
230 model: AccountModel.scope({ 239 model: AccountModel.scope({
231 method: [ AccountScopeNames.SUMMARY, { whereActor } as SummaryOptions ] 240 method: [ AccountScopeNames.SUMMARY, { whereActor, whereServer } as SummaryOptions ]
232 }), 241 }),
233 required: true 242 required: true
234 }, 243 },
@@ -349,6 +358,7 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli
349 videoChannelId?: number 358 videoChannelId?: number
350 listMyPlaylists?: boolean 359 listMyPlaylists?: boolean
351 search?: string 360 search?: string
361 host?: string
352 withVideos?: boolean // false by default 362 withVideos?: boolean // false by default
353 }) { 363 }) {
354 const query = { 364 const query = {
@@ -368,6 +378,7 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli
368 videoChannelId: options.videoChannelId, 378 videoChannelId: options.videoChannelId,
369 listMyPlaylists: options.listMyPlaylists, 379 listMyPlaylists: options.listMyPlaylists,
370 search: options.search, 380 search: options.search,
381 host: options.host,
371 withVideos: options.withVideos || false 382 withVideos: options.withVideos || false
372 } as AvailableForListOptions 383 } as AvailableForListOptions
373 ] 384 ]
@@ -390,6 +401,7 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli
390 count: number 401 count: number
391 sort: string 402 sort: string
392 search?: string 403 search?: string
404 host?: string
393 }) { 405 }) {
394 return VideoPlaylistModel.listForApi({ 406 return VideoPlaylistModel.listForApi({
395 ...options, 407 ...options,
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts
index b15d20cf9..d591a3134 100644
--- a/server/models/video/video-streaming-playlist.ts
+++ b/server/models/video/video-streaming-playlist.ts
@@ -2,7 +2,6 @@ import * as memoizee from 'memoizee'
2import { join } from 'path' 2import { join } from 'path'
3import { Op } from 'sequelize' 3import { Op } from 'sequelize'
4import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' 4import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
5import { doesExist } from '@server/helpers/database-utils'
6import { VideoFileModel } from '@server/models/video/video-file' 5import { VideoFileModel } from '@server/models/video/video-file'
7import { MStreamingPlaylist, MVideo } from '@server/types/models' 6import { MStreamingPlaylist, MVideo } from '@server/types/models'
8import { AttributesOnly } from '@shared/core-utils' 7import { AttributesOnly } from '@shared/core-utils'
@@ -20,6 +19,7 @@ import {
20 WEBSERVER 19 WEBSERVER
21} from '../../initializers/constants' 20} from '../../initializers/constants'
22import { VideoRedundancyModel } from '../redundancy/video-redundancy' 21import { VideoRedundancyModel } from '../redundancy/video-redundancy'
22import { doesExist } from '../shared'
23import { throwIfNotValid } from '../utils' 23import { throwIfNotValid } from '../utils'
24import { VideoModel } from './video' 24import { VideoModel } from './video'
25 25
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index c006a91af..c444f381e 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -24,7 +24,6 @@ import {
24 Table, 24 Table,
25 UpdatedAt 25 UpdatedAt
26} from 'sequelize-typescript' 26} from 'sequelize-typescript'
27import { setAsUpdated } from '@server/helpers/database-utils'
28import { buildNSFWFilter } from '@server/helpers/express-utils' 27import { buildNSFWFilter } from '@server/helpers/express-utils'
29import { uuidToShort } from '@server/helpers/uuid' 28import { uuidToShort } from '@server/helpers/uuid'
30import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' 29import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video'
@@ -92,6 +91,7 @@ import { VideoRedundancyModel } from '../redundancy/video-redundancy'
92import { ServerModel } from '../server/server' 91import { ServerModel } from '../server/server'
93import { TrackerModel } from '../server/tracker' 92import { TrackerModel } from '../server/tracker'
94import { VideoTrackerModel } from '../server/video-tracker' 93import { VideoTrackerModel } from '../server/video-tracker'
94import { setAsUpdated } from '../shared'
95import { UserModel } from '../user/user' 95import { UserModel } from '../user/user'
96import { UserVideoHistoryModel } from '../user/user-video-history' 96import { UserVideoHistoryModel } from '../user/user-video-history'
97import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils' 97import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'
diff --git a/server/tests/api/search/search-channels.ts b/server/tests/api/search/search-channels.ts
index 4da2d0ece..d3b0f4321 100644
--- a/server/tests/api/search/search-channels.ts
+++ b/server/tests/api/search/search-channels.ts
@@ -2,24 +2,33 @@
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { cleanupTests, createSingleServer, PeerTubeServer, SearchCommand, setAccessTokensToServers } from '@shared/extra-utils' 5import {
6 cleanupTests,
7 createSingleServer,
8 doubleFollow,
9 PeerTubeServer,
10 SearchCommand,
11 setAccessTokensToServers
12} from '@shared/extra-utils'
6import { VideoChannel } from '@shared/models' 13import { VideoChannel } from '@shared/models'
7 14
8const expect = chai.expect 15const expect = chai.expect
9 16
10describe('Test channels search', function () { 17describe('Test channels search', function () {
11 let server: PeerTubeServer = null 18 let server: PeerTubeServer
19 let remoteServer: PeerTubeServer
12 let command: SearchCommand 20 let command: SearchCommand
13 21
14 before(async function () { 22 before(async function () {
15 this.timeout(30000) 23 this.timeout(30000)
16 24
17 server = await createSingleServer(1) 25 server = await createSingleServer(1)
26 remoteServer = await createSingleServer(2, { transcoding: { enabled: false } })
18 27
19 await setAccessTokensToServers([ server ]) 28 await setAccessTokensToServers([ server, remoteServer ])
20 29
21 { 30 {
22 await server.users.create({ username: 'user1', password: 'password' }) 31 await server.users.create({ username: 'user1' })
23 const channel = { 32 const channel = {
24 name: 'squall_channel', 33 name: 'squall_channel',
25 displayName: 'Squall channel' 34 displayName: 'Squall channel'
@@ -27,6 +36,19 @@ describe('Test channels search', function () {
27 await server.channels.create({ attributes: channel }) 36 await server.channels.create({ attributes: channel })
28 } 37 }
29 38
39 {
40 await remoteServer.users.create({ username: 'user1' })
41 const channel = {
42 name: 'zell_channel',
43 displayName: 'Zell channel'
44 }
45 const { id } = await remoteServer.channels.create({ attributes: channel })
46
47 await remoteServer.videos.upload({ attributes: { channelId: id } })
48 }
49
50 await doubleFollow(server, remoteServer)
51
30 command = server.search 52 command = server.search
31 }) 53 })
32 54
@@ -66,6 +88,34 @@ describe('Test channels search', function () {
66 } 88 }
67 }) 89 })
68 90
91 it('Should filter by host', async function () {
92 {
93 const search = { search: 'channel', host: remoteServer.host }
94
95 const body = await command.advancedChannelSearch({ search })
96 expect(body.total).to.equal(1)
97 expect(body.data).to.have.lengthOf(1)
98 expect(body.data[0].displayName).to.equal('Zell channel')
99 }
100
101 {
102 const search = { search: 'Sq', host: server.host }
103
104 const body = await command.advancedChannelSearch({ search })
105 expect(body.total).to.equal(1)
106 expect(body.data).to.have.lengthOf(1)
107 expect(body.data[0].displayName).to.equal('Squall channel')
108 }
109
110 {
111 const search = { search: 'Squall', host: 'example.com' }
112
113 const body = await command.advancedChannelSearch({ search })
114 expect(body.total).to.equal(0)
115 expect(body.data).to.have.lengthOf(0)
116 }
117 })
118
69 after(async function () { 119 after(async function () {
70 await cleanupTests([ server ]) 120 await cleanupTests([ server ])
71 }) 121 })
diff --git a/server/tests/api/search/search-playlists.ts b/server/tests/api/search/search-playlists.ts
index 22e9b8fca..c3b318f5b 100644
--- a/server/tests/api/search/search-playlists.ts
+++ b/server/tests/api/search/search-playlists.ts
@@ -5,6 +5,7 @@ import * as chai from 'chai'
5import { 5import {
6 cleanupTests, 6 cleanupTests,
7 createSingleServer, 7 createSingleServer,
8 doubleFollow,
8 PeerTubeServer, 9 PeerTubeServer,
9 SearchCommand, 10 SearchCommand,
10 setAccessTokensToServers, 11 setAccessTokensToServers,
@@ -15,20 +16,22 @@ import { VideoPlaylistPrivacy } from '@shared/models'
15const expect = chai.expect 16const expect = chai.expect
16 17
17describe('Test playlists search', function () { 18describe('Test playlists search', function () {
18 let server: PeerTubeServer = null 19 let server: PeerTubeServer
20 let remoteServer: PeerTubeServer
19 let command: SearchCommand 21 let command: SearchCommand
20 22
21 before(async function () { 23 before(async function () {
22 this.timeout(30000) 24 this.timeout(30000)
23 25
24 server = await createSingleServer(1) 26 server = await createSingleServer(1)
27 remoteServer = await createSingleServer(2, { transcoding: { enabled: false } })
25 28
26 await setAccessTokensToServers([ server ]) 29 await setAccessTokensToServers([ remoteServer, server ])
27 await setDefaultVideoChannel([ server ]) 30 await setDefaultVideoChannel([ remoteServer, server ])
28
29 const videoId = (await server.videos.quickUpload({ name: 'video' })).uuid
30 31
31 { 32 {
33 const videoId = (await server.videos.upload()).uuid
34
32 const attributes = { 35 const attributes = {
33 displayName: 'Dr. Kenzo Tenma hospital videos', 36 displayName: 'Dr. Kenzo Tenma hospital videos',
34 privacy: VideoPlaylistPrivacy.PUBLIC, 37 privacy: VideoPlaylistPrivacy.PUBLIC,
@@ -40,14 +43,16 @@ describe('Test playlists search', function () {
40 } 43 }
41 44
42 { 45 {
46 const videoId = (await remoteServer.videos.upload()).uuid
47
43 const attributes = { 48 const attributes = {
44 displayName: 'Johan & Anna Libert musics', 49 displayName: 'Johan & Anna Libert music videos',
45 privacy: VideoPlaylistPrivacy.PUBLIC, 50 privacy: VideoPlaylistPrivacy.PUBLIC,
46 videoChannelId: server.store.channel.id 51 videoChannelId: remoteServer.store.channel.id
47 } 52 }
48 const created = await server.playlists.create({ attributes }) 53 const created = await remoteServer.playlists.create({ attributes })
49 54
50 await server.playlists.addElement({ playlistId: created.id, attributes: { videoId } }) 55 await remoteServer.playlists.addElement({ playlistId: created.id, attributes: { videoId } })
51 } 56 }
52 57
53 { 58 {
@@ -59,6 +64,8 @@ describe('Test playlists search', function () {
59 await server.playlists.create({ attributes }) 64 await server.playlists.create({ attributes })
60 } 65 }
61 66
67 await doubleFollow(server, remoteServer)
68
62 command = server.search 69 command = server.search
63 }) 70 })
64 71
@@ -87,7 +94,7 @@ describe('Test playlists search', function () {
87 94
88 { 95 {
89 const search = { 96 const search = {
90 search: 'Anna Livert', 97 search: 'Anna Livert music',
91 start: 0, 98 start: 0,
92 count: 1 99 count: 1
93 } 100 }
@@ -96,7 +103,36 @@ describe('Test playlists search', function () {
96 expect(body.data).to.have.lengthOf(1) 103 expect(body.data).to.have.lengthOf(1)
97 104
98 const playlist = body.data[0] 105 const playlist = body.data[0]
99 expect(playlist.displayName).to.equal('Johan & Anna Libert musics') 106 expect(playlist.displayName).to.equal('Johan & Anna Libert music videos')
107 }
108 })
109
110 it('Should filter by host', async function () {
111 {
112 const search = { search: 'tenma', host: server.host }
113 const body = await command.advancedPlaylistSearch({ search })
114 expect(body.total).to.equal(1)
115 expect(body.data).to.have.lengthOf(1)
116
117 const playlist = body.data[0]
118 expect(playlist.displayName).to.equal('Dr. Kenzo Tenma hospital videos')
119 }
120
121 {
122 const search = { search: 'Anna', host: 'example.com' }
123 const body = await command.advancedPlaylistSearch({ search })
124 expect(body.total).to.equal(0)
125 expect(body.data).to.have.lengthOf(0)
126 }
127
128 {
129 const search = { search: 'video', host: remoteServer.host }
130 const body = await command.advancedPlaylistSearch({ search })
131 expect(body.total).to.equal(1)
132 expect(body.data).to.have.lengthOf(1)
133
134 const playlist = body.data[0]
135 expect(playlist.displayName).to.equal('Johan & Anna Libert music videos')
100 } 136 }
101 }) 137 })
102 138
diff --git a/shared/models/search/video-channels-search-query.model.ts b/shared/models/search/video-channels-search-query.model.ts
index 8f93c4bd5..2622dfbc6 100644
--- a/shared/models/search/video-channels-search-query.model.ts
+++ b/shared/models/search/video-channels-search-query.model.ts
@@ -6,4 +6,6 @@ export interface VideoChannelsSearchQuery extends SearchTargetQuery {
6 start?: number 6 start?: number
7 count?: number 7 count?: number
8 sort?: string 8 sort?: string
9
10 host?: string
9} 11}
diff --git a/shared/models/search/video-playlists-search-query.model.ts b/shared/models/search/video-playlists-search-query.model.ts
index 31f05218e..dcf66e9e3 100644
--- a/shared/models/search/video-playlists-search-query.model.ts
+++ b/shared/models/search/video-playlists-search-query.model.ts
@@ -6,4 +6,6 @@ export interface VideoPlaylistsSearchQuery extends SearchTargetQuery {
6 start?: number 6 start?: number
7 count?: number 7 count?: number
8 sort?: string 8 sort?: string
9
10 host?: string
9} 11}