diff options
author | Chocobozzz <me@florianbigard.com> | 2021-07-28 10:32:40 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2021-07-28 10:32:40 +0200 |
commit | fa47956ecf51a6d5d10aeb25d2e4db3da90c7d58 (patch) | |
tree | bd626648077f84fb4628af3a37acf260597fa0ef /server | |
parent | f68d1cb6ac4aa4fb563b9eeb831fccffee260b2f (diff) | |
download | PeerTube-fa47956ecf51a6d5d10aeb25d2e4db3da90c7d58.tar.gz PeerTube-fa47956ecf51a6d5d10aeb25d2e4db3da90c7d58.tar.zst PeerTube-fa47956ecf51a6d5d10aeb25d2e4db3da90c7d58.zip |
Filter host for channels and playlists search
Diffstat (limited to 'server')
-rw-r--r-- | server/controllers/api/search/search-video-channels.ts | 3 | ||||
-rw-r--r-- | server/controllers/api/search/search-video-playlists.ts | 3 | ||||
-rw-r--r-- | server/helpers/database-utils.ts | 31 | ||||
-rw-r--r-- | server/middlewares/validators/search.ts | 18 | ||||
-rw-r--r-- | server/models/account/account.ts | 8 | ||||
-rw-r--r-- | server/models/actor/actor-follow.ts | 2 | ||||
-rw-r--r-- | server/models/shared/index.ts | 2 | ||||
-rw-r--r-- | server/models/shared/query.ts | 17 | ||||
-rw-r--r-- | server/models/shared/update.ts | 18 | ||||
-rw-r--r-- | server/models/video/video-channel.ts | 59 | ||||
-rw-r--r-- | server/models/video/video-file.ts | 2 | ||||
-rw-r--r-- | server/models/video/video-playlist.ts | 24 | ||||
-rw-r--r-- | server/models/video/video-streaming-playlist.ts | 2 | ||||
-rw-r--r-- | server/models/video/video.ts | 2 | ||||
-rw-r--r-- | server/tests/api/search/search-channels.ts | 58 | ||||
-rw-r--r-- | server/tests/api/search/search-playlists.ts | 58 |
16 files changed, 230 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 @@ | |||
1 | import * as retry from 'async/retry' | 1 | import * as retry from 'async/retry' |
2 | import * as Bluebird from 'bluebird' | 2 | import * as Bluebird from 'bluebird' |
3 | import { BindOrReplacements, QueryTypes, Transaction } from 'sequelize' | 3 | import { Transaction } from 'sequelize' |
4 | import { Model } from 'sequelize-typescript' | 4 | import { Model } from 'sequelize-typescript' |
5 | import { sequelizeTypescript } from '@server/initializers/database' | 5 | import { sequelizeTypescript } from '@server/initializers/database' |
6 | import { logger } from './logger' | 6 | import { 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 | ||
99 | function 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 | ||
112 | function runInReadCommittedTransaction <T> (fn: (t: Transaction) => Promise<T>) { | 100 | function runInReadCommittedTransaction <T> (fn: (t: Transaction) => Promise<T>) { |
@@ -123,19 +111,6 @@ function afterCommitIfTransaction (t: Transaction, fn: Function) { | |||
123 | 111 | ||
124 | // --------------------------------------------------------------------------- | 112 | // --------------------------------------------------------------------------- |
125 | 113 | ||
126 | function 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 | |||
139 | export { | 114 | export { |
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 | ||
44 | const videoChannelsListSearchValidator = [ | 44 | const 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 | ||
57 | const videoPlaylistsListSearchValidator = [ | 64 | const 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 { | |||
52 | export type SummaryOptions = { | 52 | export 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' |
21 | import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activitypub/misc' | 21 | import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activitypub/misc' |
22 | import { doesExist } from '@server/helpers/database-utils' | ||
23 | import { getServerActor } from '@server/models/application/application' | 22 | import { getServerActor } from '@server/models/application/application' |
24 | import { | 23 | import { |
25 | MActorFollowActorsDefault, | 24 | MActorFollowActorsDefault, |
@@ -36,6 +35,7 @@ import { logger } from '../../helpers/logger' | |||
36 | import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants' | 35 | import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants' |
37 | import { AccountModel } from '../account/account' | 36 | import { AccountModel } from '../account/account' |
38 | import { ServerModel } from '../server/server' | 37 | import { ServerModel } from '../server/server' |
38 | import { doesExist } from '../shared/query' | ||
39 | import { createSafeIn, getFollowsSort, getSort, searchAttribute, throwIfNotValid } from '../utils' | 39 | import { createSafeIn, getFollowsSort, getSort, searchAttribute, throwIfNotValid } from '../utils' |
40 | import { VideoChannelModel } from '../video/video-channel' | 40 | import { VideoChannelModel } from '../video/video-channel' |
41 | import { ActorModel, unusedActorAttributesForAPI } from './actor' | 41 | import { 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 @@ | |||
1 | export * from './query' | ||
2 | export * 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 @@ | |||
1 | import { BindOrReplacements, QueryTypes } from 'sequelize' | ||
2 | import { sequelizeTypescript } from '@server/initializers/database' | ||
3 | |||
4 | function 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 | |||
15 | export { | ||
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 @@ | |||
1 | import { QueryTypes, Transaction } from 'sequelize' | ||
2 | import { sequelizeTypescript } from '@server/initializers/database' | ||
3 | |||
4 | // Sequelize always skip the update if we only update updatedAt field | ||
5 | function 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 | |||
16 | export { | ||
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 @@ | |||
1 | import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions, Transaction } from 'sequelize' | 1 | import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions, Transaction, WhereOptions } from 'sequelize' |
2 | import { | 2 | import { |
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' |
20 | import { setAsUpdated } from '@server/helpers/database-utils' | ||
21 | import { MAccountActor } from '@server/types/models' | 20 | import { MAccountActor } from '@server/types/models' |
22 | import { AttributesOnly } from '@shared/core-utils' | 21 | import { AttributesOnly } from '@shared/core-utils' |
23 | import { ActivityPubActor } from '../../../shared/models/activitypub' | 22 | import { ActivityPubActor } from '../../../shared/models/activitypub' |
@@ -41,6 +40,7 @@ import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor' | |||
41 | import { ActorFollowModel } from '../actor/actor-follow' | 40 | import { ActorFollowModel } from '../actor/actor-follow' |
42 | import { ActorImageModel } from '../actor/actor-image' | 41 | import { ActorImageModel } from '../actor/actor-image' |
43 | import { ServerModel } from '../server/server' | 42 | import { ServerModel } from '../server/server' |
43 | import { setAsUpdated } from '../shared' | ||
44 | import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' | 44 | import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' |
45 | import { VideoModel } from './video' | 45 | import { VideoModel } from './video' |
46 | import { VideoPlaylistModel } from './video-playlist' | 46 | import { VideoPlaylistModel } from './video-playlist' |
@@ -58,6 +58,7 @@ export enum ScopeNames { | |||
58 | type AvailableForListOptions = { | 58 | type AvailableForListOptions = { |
59 | actorId: number | 59 | actorId: number |
60 | search?: string | 60 | search?: string |
61 | host?: string | ||
61 | } | 62 | } |
62 | 63 | ||
63 | type AvailableWithStatsOptions = { | 64 | type 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 { | |||
21 | import { Where } from 'sequelize/types/lib/utils' | 21 | import { Where } from 'sequelize/types/lib/utils' |
22 | import validator from 'validator' | 22 | import validator from 'validator' |
23 | import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub' | 23 | import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub' |
24 | import { doesExist } from '@server/helpers/database-utils' | ||
25 | import { logger } from '@server/helpers/logger' | 24 | import { logger } from '@server/helpers/logger' |
26 | import { extractVideo } from '@server/helpers/video' | 25 | import { extractVideo } from '@server/helpers/video' |
27 | import { getTorrentFilePath } from '@server/lib/video-paths' | 26 | import { getTorrentFilePath } from '@server/lib/video-paths' |
@@ -45,6 +44,7 @@ import { | |||
45 | } from '../../initializers/constants' | 44 | } from '../../initializers/constants' |
46 | import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file' | 45 | import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file' |
47 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 46 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
47 | import { doesExist } from '../shared' | ||
48 | import { parseAggregateResult, throwIfNotValid } from '../utils' | 48 | import { parseAggregateResult, throwIfNotValid } from '../utils' |
49 | import { VideoModel } from './video' | 49 | import { VideoModel } from './video' |
50 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | 50 | import { 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' |
20 | import { setAsUpdated } from '@server/helpers/database-utils' | ||
21 | import { buildUUID, uuidToShort } from '@server/helpers/uuid' | 20 | import { buildUUID, uuidToShort } from '@server/helpers/uuid' |
22 | import { MAccountId, MChannelId } from '@server/types/models' | 21 | import { MAccountId, MChannelId } from '@server/types/models' |
23 | import { AttributesOnly, buildPlaylistEmbedPath, buildPlaylistWatchPath } from '@shared/core-utils' | 22 | import { 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' |
54 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' | 53 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' |
55 | import { ActorModel } from '../actor/actor' | 54 | import { ActorModel } from '../actor/actor' |
55 | import { setAsUpdated } from '../shared' | ||
56 | import { | 56 | import { |
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' | |||
2 | import { join } from 'path' | 2 | import { join } from 'path' |
3 | import { Op } from 'sequelize' | 3 | import { Op } from 'sequelize' |
4 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | 4 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
5 | import { doesExist } from '@server/helpers/database-utils' | ||
6 | import { VideoFileModel } from '@server/models/video/video-file' | 5 | import { VideoFileModel } from '@server/models/video/video-file' |
7 | import { MStreamingPlaylist, MVideo } from '@server/types/models' | 6 | import { MStreamingPlaylist, MVideo } from '@server/types/models' |
8 | import { AttributesOnly } from '@shared/core-utils' | 7 | import { AttributesOnly } from '@shared/core-utils' |
@@ -20,6 +19,7 @@ import { | |||
20 | WEBSERVER | 19 | WEBSERVER |
21 | } from '../../initializers/constants' | 20 | } from '../../initializers/constants' |
22 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 21 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
22 | import { doesExist } from '../shared' | ||
23 | import { throwIfNotValid } from '../utils' | 23 | import { throwIfNotValid } from '../utils' |
24 | import { VideoModel } from './video' | 24 | import { 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' |
27 | import { setAsUpdated } from '@server/helpers/database-utils' | ||
28 | import { buildNSFWFilter } from '@server/helpers/express-utils' | 27 | import { buildNSFWFilter } from '@server/helpers/express-utils' |
29 | import { uuidToShort } from '@server/helpers/uuid' | 28 | import { uuidToShort } from '@server/helpers/uuid' |
30 | import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' | 29 | import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' |
@@ -92,6 +91,7 @@ import { VideoRedundancyModel } from '../redundancy/video-redundancy' | |||
92 | import { ServerModel } from '../server/server' | 91 | import { ServerModel } from '../server/server' |
93 | import { TrackerModel } from '../server/tracker' | 92 | import { TrackerModel } from '../server/tracker' |
94 | import { VideoTrackerModel } from '../server/video-tracker' | 93 | import { VideoTrackerModel } from '../server/video-tracker' |
94 | import { setAsUpdated } from '../shared' | ||
95 | import { UserModel } from '../user/user' | 95 | import { UserModel } from '../user/user' |
96 | import { UserVideoHistoryModel } from '../user/user-video-history' | 96 | import { UserVideoHistoryModel } from '../user/user-video-history' |
97 | import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils' | 97 | import { 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 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { cleanupTests, createSingleServer, PeerTubeServer, SearchCommand, setAccessTokensToServers } from '@shared/extra-utils' | 5 | import { |
6 | cleanupTests, | ||
7 | createSingleServer, | ||
8 | doubleFollow, | ||
9 | PeerTubeServer, | ||
10 | SearchCommand, | ||
11 | setAccessTokensToServers | ||
12 | } from '@shared/extra-utils' | ||
6 | import { VideoChannel } from '@shared/models' | 13 | import { VideoChannel } from '@shared/models' |
7 | 14 | ||
8 | const expect = chai.expect | 15 | const expect = chai.expect |
9 | 16 | ||
10 | describe('Test channels search', function () { | 17 | describe('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' | |||
5 | import { | 5 | import { |
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' | |||
15 | const expect = chai.expect | 16 | const expect = chai.expect |
16 | 17 | ||
17 | describe('Test playlists search', function () { | 18 | describe('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 | ||