diff options
author | Chocobozzz <me@florianbigard.com> | 2021-06-17 16:02:38 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2021-06-25 14:44:01 +0200 |
commit | 37a44fc915eef2140e22ceb96aba6b6eb2509007 (patch) | |
tree | dd4a370ecc96cf38c99b940261aadc27065da7ae /server/controllers/api | |
parent | 33eb19e5199cc9fa4d73c6675c97508e3e072ef9 (diff) | |
download | PeerTube-37a44fc915eef2140e22ceb96aba6b6eb2509007.tar.gz PeerTube-37a44fc915eef2140e22ceb96aba6b6eb2509007.tar.zst PeerTube-37a44fc915eef2140e22ceb96aba6b6eb2509007.zip |
Add ability to search playlists
Diffstat (limited to 'server/controllers/api')
-rw-r--r-- | server/controllers/api/search.ts | 294 | ||||
-rw-r--r-- | server/controllers/api/search/index.ts | 16 | ||||
-rw-r--r-- | server/controllers/api/search/search-video-channels.ts | 150 | ||||
-rw-r--r-- | server/controllers/api/search/search-video-playlists.ts | 129 | ||||
-rw-r--r-- | server/controllers/api/search/search-videos.ts | 153 | ||||
-rw-r--r-- | server/controllers/api/video-channel.ts | 4 | ||||
-rw-r--r-- | server/controllers/api/video-playlist.ts | 8 |
7 files changed, 453 insertions, 301 deletions
diff --git a/server/controllers/api/search.ts b/server/controllers/api/search.ts deleted file mode 100644 index c975c5c3c..000000000 --- a/server/controllers/api/search.ts +++ /dev/null | |||
@@ -1,294 +0,0 @@ | |||
1 | import * as express from 'express' | ||
2 | import { sanitizeUrl } from '@server/helpers/core-utils' | ||
3 | import { doJSONRequest } from '@server/helpers/requests' | ||
4 | import { CONFIG } from '@server/initializers/config' | ||
5 | import { getOrCreateAPVideo } from '@server/lib/activitypub/videos' | ||
6 | import { Hooks } from '@server/lib/plugins/hooks' | ||
7 | import { AccountBlocklistModel } from '@server/models/account/account-blocklist' | ||
8 | import { getServerActor } from '@server/models/application/application' | ||
9 | import { ServerBlocklistModel } from '@server/models/server/server-blocklist' | ||
10 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | ||
11 | import { ResultList, Video, VideoChannel } from '@shared/models' | ||
12 | import { SearchTargetQuery } from '@shared/models/search/search-target-query.model' | ||
13 | import { VideoChannelsSearchQuery, VideosSearchQuery } from '../../../shared/models/search' | ||
14 | import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' | ||
15 | import { logger } from '../../helpers/logger' | ||
16 | import { getFormattedObjects } from '../../helpers/utils' | ||
17 | import { getOrCreateAPActor, loadActorUrlOrGetFromWebfinger } from '../../lib/activitypub/actors' | ||
18 | import { | ||
19 | asyncMiddleware, | ||
20 | commonVideosFiltersValidator, | ||
21 | openapiOperationDoc, | ||
22 | optionalAuthenticate, | ||
23 | paginationValidator, | ||
24 | setDefaultPagination, | ||
25 | setDefaultSearchSort, | ||
26 | videoChannelsListSearchValidator, | ||
27 | videoChannelsSearchSortValidator, | ||
28 | videosSearchSortValidator, | ||
29 | videosSearchValidator | ||
30 | } from '../../middlewares' | ||
31 | import { VideoModel } from '../../models/video/video' | ||
32 | import { VideoChannelModel } from '../../models/video/video-channel' | ||
33 | import { MChannelAccountDefault, MVideoAccountLightBlacklistAllFiles } from '../../types/models' | ||
34 | |||
35 | const searchRouter = express.Router() | ||
36 | |||
37 | searchRouter.get('/videos', | ||
38 | openapiOperationDoc({ operationId: 'searchVideos' }), | ||
39 | paginationValidator, | ||
40 | setDefaultPagination, | ||
41 | videosSearchSortValidator, | ||
42 | setDefaultSearchSort, | ||
43 | optionalAuthenticate, | ||
44 | commonVideosFiltersValidator, | ||
45 | videosSearchValidator, | ||
46 | asyncMiddleware(searchVideos) | ||
47 | ) | ||
48 | |||
49 | searchRouter.get('/video-channels', | ||
50 | openapiOperationDoc({ operationId: 'searchChannels' }), | ||
51 | paginationValidator, | ||
52 | setDefaultPagination, | ||
53 | videoChannelsSearchSortValidator, | ||
54 | setDefaultSearchSort, | ||
55 | optionalAuthenticate, | ||
56 | videoChannelsListSearchValidator, | ||
57 | asyncMiddleware(searchVideoChannels) | ||
58 | ) | ||
59 | |||
60 | // --------------------------------------------------------------------------- | ||
61 | |||
62 | export { searchRouter } | ||
63 | |||
64 | // --------------------------------------------------------------------------- | ||
65 | |||
66 | function searchVideoChannels (req: express.Request, res: express.Response) { | ||
67 | const query: VideoChannelsSearchQuery = req.query | ||
68 | const search = query.search | ||
69 | |||
70 | const isURISearch = search.startsWith('http://') || search.startsWith('https://') | ||
71 | |||
72 | const parts = search.split('@') | ||
73 | |||
74 | // Handle strings like @toto@example.com | ||
75 | if (parts.length === 3 && parts[0].length === 0) parts.shift() | ||
76 | const isWebfingerSearch = parts.length === 2 && parts.every(p => p && !p.includes(' ')) | ||
77 | |||
78 | if (isURISearch || isWebfingerSearch) return searchVideoChannelURI(search, isWebfingerSearch, res) | ||
79 | |||
80 | // @username -> username to search in DB | ||
81 | if (query.search.startsWith('@')) query.search = query.search.replace(/^@/, '') | ||
82 | |||
83 | if (isSearchIndexSearch(query)) { | ||
84 | return searchVideoChannelsIndex(query, res) | ||
85 | } | ||
86 | |||
87 | return searchVideoChannelsDB(query, res) | ||
88 | } | ||
89 | |||
90 | async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: express.Response) { | ||
91 | const result = await buildMutedForSearchIndex(res) | ||
92 | |||
93 | const body = await Hooks.wrapObject(Object.assign(query, result), 'filter:api.search.video-channels.index.list.params') | ||
94 | |||
95 | const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/video-channels' | ||
96 | |||
97 | try { | ||
98 | logger.debug('Doing video channels search index request on %s.', url, { body }) | ||
99 | |||
100 | const { body: searchIndexResult } = await doJSONRequest<ResultList<VideoChannel>>(url, { method: 'POST', json: body }) | ||
101 | const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.video-channels.index.list.result') | ||
102 | |||
103 | return res.json(jsonResult) | ||
104 | } catch (err) { | ||
105 | logger.warn('Cannot use search index to make video channels search.', { err }) | ||
106 | |||
107 | return res.fail({ | ||
108 | status: HttpStatusCode.INTERNAL_SERVER_ERROR_500, | ||
109 | message: 'Cannot use search index to make video channels search' | ||
110 | }) | ||
111 | } | ||
112 | } | ||
113 | |||
114 | async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: express.Response) { | ||
115 | const serverActor = await getServerActor() | ||
116 | |||
117 | const apiOptions = await Hooks.wrapObject({ | ||
118 | actorId: serverActor.id, | ||
119 | search: query.search, | ||
120 | start: query.start, | ||
121 | count: query.count, | ||
122 | sort: query.sort | ||
123 | }, 'filter:api.search.video-channels.local.list.params') | ||
124 | |||
125 | const resultList = await Hooks.wrapPromiseFun( | ||
126 | VideoChannelModel.searchForApi, | ||
127 | apiOptions, | ||
128 | 'filter:api.search.video-channels.local.list.result' | ||
129 | ) | ||
130 | |||
131 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
132 | } | ||
133 | |||
134 | async function searchVideoChannelURI (search: string, isWebfingerSearch: boolean, res: express.Response) { | ||
135 | let videoChannel: MChannelAccountDefault | ||
136 | let uri = search | ||
137 | |||
138 | if (isWebfingerSearch) { | ||
139 | try { | ||
140 | uri = await loadActorUrlOrGetFromWebfinger(search) | ||
141 | } catch (err) { | ||
142 | logger.warn('Cannot load actor URL or get from webfinger.', { search, err }) | ||
143 | |||
144 | return res.json({ total: 0, data: [] }) | ||
145 | } | ||
146 | } | ||
147 | |||
148 | if (isUserAbleToSearchRemoteURI(res)) { | ||
149 | try { | ||
150 | const actor = await getOrCreateAPActor(uri, 'all', true, true) | ||
151 | videoChannel = actor.VideoChannel | ||
152 | } catch (err) { | ||
153 | logger.info('Cannot search remote video channel %s.', uri, { err }) | ||
154 | } | ||
155 | } else { | ||
156 | videoChannel = await VideoChannelModel.loadByUrlAndPopulateAccount(uri) | ||
157 | } | ||
158 | |||
159 | return res.json({ | ||
160 | total: videoChannel ? 1 : 0, | ||
161 | data: videoChannel ? [ videoChannel.toFormattedJSON() ] : [] | ||
162 | }) | ||
163 | } | ||
164 | |||
165 | function searchVideos (req: express.Request, res: express.Response) { | ||
166 | const query: VideosSearchQuery = req.query | ||
167 | const search = query.search | ||
168 | |||
169 | if (search && (search.startsWith('http://') || search.startsWith('https://'))) { | ||
170 | return searchVideoURI(search, res) | ||
171 | } | ||
172 | |||
173 | if (isSearchIndexSearch(query)) { | ||
174 | return searchVideosIndex(query, res) | ||
175 | } | ||
176 | |||
177 | return searchVideosDB(query, res) | ||
178 | } | ||
179 | |||
180 | async function searchVideosIndex (query: VideosSearchQuery, res: express.Response) { | ||
181 | const result = await buildMutedForSearchIndex(res) | ||
182 | |||
183 | let body: VideosSearchQuery = Object.assign(query, result) | ||
184 | |||
185 | // Use the default instance NSFW policy if not specified | ||
186 | if (!body.nsfw) { | ||
187 | const nsfwPolicy = res.locals.oauth | ||
188 | ? res.locals.oauth.token.User.nsfwPolicy | ||
189 | : CONFIG.INSTANCE.DEFAULT_NSFW_POLICY | ||
190 | |||
191 | body.nsfw = nsfwPolicy === 'do_not_list' | ||
192 | ? 'false' | ||
193 | : 'both' | ||
194 | } | ||
195 | |||
196 | body = await Hooks.wrapObject(body, 'filter:api.search.videos.index.list.params') | ||
197 | |||
198 | const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/videos' | ||
199 | |||
200 | try { | ||
201 | logger.debug('Doing videos search index request on %s.', url, { body }) | ||
202 | |||
203 | const { body: searchIndexResult } = await doJSONRequest<ResultList<Video>>(url, { method: 'POST', json: body }) | ||
204 | const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.videos.index.list.result') | ||
205 | |||
206 | return res.json(jsonResult) | ||
207 | } catch (err) { | ||
208 | logger.warn('Cannot use search index to make video search.', { err }) | ||
209 | |||
210 | return res.fail({ | ||
211 | status: HttpStatusCode.INTERNAL_SERVER_ERROR_500, | ||
212 | message: 'Cannot use search index to make video search' | ||
213 | }) | ||
214 | } | ||
215 | } | ||
216 | |||
217 | async function searchVideosDB (query: VideosSearchQuery, res: express.Response) { | ||
218 | const apiOptions = await Hooks.wrapObject(Object.assign(query, { | ||
219 | includeLocalVideos: true, | ||
220 | nsfw: buildNSFWFilter(res, query.nsfw), | ||
221 | filter: query.filter, | ||
222 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined | ||
223 | }), 'filter:api.search.videos.local.list.params') | ||
224 | |||
225 | const resultList = await Hooks.wrapPromiseFun( | ||
226 | VideoModel.searchAndPopulateAccountAndServer, | ||
227 | apiOptions, | ||
228 | 'filter:api.search.videos.local.list.result' | ||
229 | ) | ||
230 | |||
231 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
232 | } | ||
233 | |||
234 | async function searchVideoURI (url: string, res: express.Response) { | ||
235 | let video: MVideoAccountLightBlacklistAllFiles | ||
236 | |||
237 | // Check if we can fetch a remote video with the URL | ||
238 | if (isUserAbleToSearchRemoteURI(res)) { | ||
239 | try { | ||
240 | const syncParam = { | ||
241 | likes: false, | ||
242 | dislikes: false, | ||
243 | shares: false, | ||
244 | comments: false, | ||
245 | thumbnail: true, | ||
246 | refreshVideo: false | ||
247 | } | ||
248 | |||
249 | const result = await getOrCreateAPVideo({ videoObject: url, syncParam }) | ||
250 | video = result ? result.video : undefined | ||
251 | } catch (err) { | ||
252 | logger.info('Cannot search remote video %s.', url, { err }) | ||
253 | } | ||
254 | } else { | ||
255 | video = await VideoModel.loadByUrlAndPopulateAccount(url) | ||
256 | } | ||
257 | |||
258 | return res.json({ | ||
259 | total: video ? 1 : 0, | ||
260 | data: video ? [ video.toFormattedJSON() ] : [] | ||
261 | }) | ||
262 | } | ||
263 | |||
264 | function isSearchIndexSearch (query: SearchTargetQuery) { | ||
265 | if (query.searchTarget === 'search-index') return true | ||
266 | |||
267 | const searchIndexConfig = CONFIG.SEARCH.SEARCH_INDEX | ||
268 | |||
269 | if (searchIndexConfig.ENABLED !== true) return false | ||
270 | |||
271 | if (searchIndexConfig.DISABLE_LOCAL_SEARCH) return true | ||
272 | if (searchIndexConfig.IS_DEFAULT_SEARCH && !query.searchTarget) return true | ||
273 | |||
274 | return false | ||
275 | } | ||
276 | |||
277 | async function buildMutedForSearchIndex (res: express.Response) { | ||
278 | const serverActor = await getServerActor() | ||
279 | const accountIds = [ serverActor.Account.id ] | ||
280 | |||
281 | if (res.locals.oauth) { | ||
282 | accountIds.push(res.locals.oauth.token.User.Account.id) | ||
283 | } | ||
284 | |||
285 | const [ blockedHosts, blockedAccounts ] = await Promise.all([ | ||
286 | ServerBlocklistModel.listHostsBlockedBy(accountIds), | ||
287 | AccountBlocklistModel.listHandlesBlockedBy(accountIds) | ||
288 | ]) | ||
289 | |||
290 | return { | ||
291 | blockedHosts, | ||
292 | blockedAccounts | ||
293 | } | ||
294 | } | ||
diff --git a/server/controllers/api/search/index.ts b/server/controllers/api/search/index.ts new file mode 100644 index 000000000..67adbb307 --- /dev/null +++ b/server/controllers/api/search/index.ts | |||
@@ -0,0 +1,16 @@ | |||
1 | import * as express from 'express' | ||
2 | import { searchChannelsRouter } from './search-video-channels' | ||
3 | import { searchPlaylistsRouter } from './search-video-playlists' | ||
4 | import { searchVideosRouter } from './search-videos' | ||
5 | |||
6 | const searchRouter = express.Router() | ||
7 | |||
8 | searchRouter.use('/', searchVideosRouter) | ||
9 | searchRouter.use('/', searchChannelsRouter) | ||
10 | searchRouter.use('/', searchPlaylistsRouter) | ||
11 | |||
12 | // --------------------------------------------------------------------------- | ||
13 | |||
14 | export { | ||
15 | searchRouter | ||
16 | } | ||
diff --git a/server/controllers/api/search/search-video-channels.ts b/server/controllers/api/search/search-video-channels.ts new file mode 100644 index 000000000..16beeed60 --- /dev/null +++ b/server/controllers/api/search/search-video-channels.ts | |||
@@ -0,0 +1,150 @@ | |||
1 | import * as express from 'express' | ||
2 | import { sanitizeUrl } from '@server/helpers/core-utils' | ||
3 | import { doJSONRequest } from '@server/helpers/requests' | ||
4 | import { CONFIG } from '@server/initializers/config' | ||
5 | import { WEBSERVER } from '@server/initializers/constants' | ||
6 | import { Hooks } from '@server/lib/plugins/hooks' | ||
7 | import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search' | ||
8 | import { getServerActor } from '@server/models/application/application' | ||
9 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | ||
10 | import { ResultList, VideoChannel } from '@shared/models' | ||
11 | import { VideoChannelsSearchQuery } from '../../../../shared/models/search' | ||
12 | import { isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils' | ||
13 | import { logger } from '../../../helpers/logger' | ||
14 | import { getFormattedObjects } from '../../../helpers/utils' | ||
15 | import { getOrCreateAPActor, loadActorUrlOrGetFromWebfinger } from '../../../lib/activitypub/actors' | ||
16 | import { | ||
17 | asyncMiddleware, | ||
18 | openapiOperationDoc, | ||
19 | optionalAuthenticate, | ||
20 | paginationValidator, | ||
21 | setDefaultPagination, | ||
22 | setDefaultSearchSort, | ||
23 | videoChannelsListSearchValidator, | ||
24 | videoChannelsSearchSortValidator | ||
25 | } from '../../../middlewares' | ||
26 | import { VideoChannelModel } from '../../../models/video/video-channel' | ||
27 | import { MChannelAccountDefault } from '../../../types/models' | ||
28 | |||
29 | const searchChannelsRouter = express.Router() | ||
30 | |||
31 | searchChannelsRouter.get('/video-channels', | ||
32 | openapiOperationDoc({ operationId: 'searchChannels' }), | ||
33 | paginationValidator, | ||
34 | setDefaultPagination, | ||
35 | videoChannelsSearchSortValidator, | ||
36 | setDefaultSearchSort, | ||
37 | optionalAuthenticate, | ||
38 | videoChannelsListSearchValidator, | ||
39 | asyncMiddleware(searchVideoChannels) | ||
40 | ) | ||
41 | |||
42 | // --------------------------------------------------------------------------- | ||
43 | |||
44 | export { searchChannelsRouter } | ||
45 | |||
46 | // --------------------------------------------------------------------------- | ||
47 | |||
48 | function searchVideoChannels (req: express.Request, res: express.Response) { | ||
49 | const query: VideoChannelsSearchQuery = req.query | ||
50 | const search = query.search | ||
51 | |||
52 | const parts = search.split('@') | ||
53 | |||
54 | // Handle strings like @toto@example.com | ||
55 | if (parts.length === 3 && parts[0].length === 0) parts.shift() | ||
56 | const isWebfingerSearch = parts.length === 2 && parts.every(p => p && !p.includes(' ')) | ||
57 | |||
58 | if (isURISearch(search) || isWebfingerSearch) return searchVideoChannelURI(search, isWebfingerSearch, res) | ||
59 | |||
60 | // @username -> username to search in DB | ||
61 | if (query.search.startsWith('@')) query.search = query.search.replace(/^@/, '') | ||
62 | |||
63 | if (isSearchIndexSearch(query)) { | ||
64 | return searchVideoChannelsIndex(query, res) | ||
65 | } | ||
66 | |||
67 | return searchVideoChannelsDB(query, res) | ||
68 | } | ||
69 | |||
70 | async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: express.Response) { | ||
71 | const result = await buildMutedForSearchIndex(res) | ||
72 | |||
73 | const body = await Hooks.wrapObject(Object.assign(query, result), 'filter:api.search.video-channels.index.list.params') | ||
74 | |||
75 | const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/video-channels' | ||
76 | |||
77 | try { | ||
78 | logger.debug('Doing video channels search index request on %s.', url, { body }) | ||
79 | |||
80 | const { body: searchIndexResult } = await doJSONRequest<ResultList<VideoChannel>>(url, { method: 'POST', json: body }) | ||
81 | const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.video-channels.index.list.result') | ||
82 | |||
83 | return res.json(jsonResult) | ||
84 | } catch (err) { | ||
85 | logger.warn('Cannot use search index to make video channels search.', { err }) | ||
86 | |||
87 | return res.fail({ | ||
88 | status: HttpStatusCode.INTERNAL_SERVER_ERROR_500, | ||
89 | message: 'Cannot use search index to make video channels search' | ||
90 | }) | ||
91 | } | ||
92 | } | ||
93 | |||
94 | async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: express.Response) { | ||
95 | const serverActor = await getServerActor() | ||
96 | |||
97 | const apiOptions = await Hooks.wrapObject({ | ||
98 | actorId: serverActor.id, | ||
99 | search: query.search, | ||
100 | start: query.start, | ||
101 | count: query.count, | ||
102 | sort: query.sort | ||
103 | }, 'filter:api.search.video-channels.local.list.params') | ||
104 | |||
105 | const resultList = await Hooks.wrapPromiseFun( | ||
106 | VideoChannelModel.searchForApi, | ||
107 | apiOptions, | ||
108 | 'filter:api.search.video-channels.local.list.result' | ||
109 | ) | ||
110 | |||
111 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
112 | } | ||
113 | |||
114 | async function searchVideoChannelURI (search: string, isWebfingerSearch: boolean, res: express.Response) { | ||
115 | let videoChannel: MChannelAccountDefault | ||
116 | let uri = search | ||
117 | |||
118 | if (isWebfingerSearch) { | ||
119 | try { | ||
120 | uri = await loadActorUrlOrGetFromWebfinger(search) | ||
121 | } catch (err) { | ||
122 | logger.warn('Cannot load actor URL or get from webfinger.', { search, err }) | ||
123 | |||
124 | return res.json({ total: 0, data: [] }) | ||
125 | } | ||
126 | } | ||
127 | |||
128 | if (isUserAbleToSearchRemoteURI(res)) { | ||
129 | try { | ||
130 | const actor = await getOrCreateAPActor(uri, 'all', true, true) | ||
131 | videoChannel = actor.VideoChannel | ||
132 | } catch (err) { | ||
133 | logger.info('Cannot search remote video channel %s.', uri, { err }) | ||
134 | } | ||
135 | } else { | ||
136 | videoChannel = await VideoChannelModel.loadByUrlAndPopulateAccount(sanitizeLocalUrl(uri)) | ||
137 | } | ||
138 | |||
139 | return res.json({ | ||
140 | total: videoChannel ? 1 : 0, | ||
141 | data: videoChannel ? [ videoChannel.toFormattedJSON() ] : [] | ||
142 | }) | ||
143 | } | ||
144 | |||
145 | function sanitizeLocalUrl (url: string) { | ||
146 | if (!url) return '' | ||
147 | |||
148 | // Handle alternative channel URLs | ||
149 | return url.replace(new RegExp('^' + WEBSERVER.URL + '/c/'), WEBSERVER.URL + '/video-channels/') | ||
150 | } | ||
diff --git a/server/controllers/api/search/search-video-playlists.ts b/server/controllers/api/search/search-video-playlists.ts new file mode 100644 index 000000000..b231ff1e2 --- /dev/null +++ b/server/controllers/api/search/search-video-playlists.ts | |||
@@ -0,0 +1,129 @@ | |||
1 | import * as express from 'express' | ||
2 | import { sanitizeUrl } from '@server/helpers/core-utils' | ||
3 | import { isUserAbleToSearchRemoteURI } from '@server/helpers/express-utils' | ||
4 | import { logger } from '@server/helpers/logger' | ||
5 | import { doJSONRequest } from '@server/helpers/requests' | ||
6 | import { getFormattedObjects } from '@server/helpers/utils' | ||
7 | import { CONFIG } from '@server/initializers/config' | ||
8 | import { getOrCreateAPVideoPlaylist } from '@server/lib/activitypub/playlists/get' | ||
9 | import { Hooks } from '@server/lib/plugins/hooks' | ||
10 | import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search' | ||
11 | import { getServerActor } from '@server/models/application/application' | ||
12 | import { VideoPlaylistModel } from '@server/models/video/video-playlist' | ||
13 | import { MVideoPlaylistFullSummary } from '@server/types/models' | ||
14 | import { HttpStatusCode } from '@shared/core-utils' | ||
15 | import { ResultList, VideoPlaylist, VideoPlaylistsSearchQuery } from '@shared/models' | ||
16 | import { | ||
17 | asyncMiddleware, | ||
18 | openapiOperationDoc, | ||
19 | optionalAuthenticate, | ||
20 | paginationValidator, | ||
21 | setDefaultPagination, | ||
22 | setDefaultSearchSort, | ||
23 | videoPlaylistsListSearchValidator, | ||
24 | videoPlaylistsSearchSortValidator | ||
25 | } from '../../../middlewares' | ||
26 | import { WEBSERVER } from '@server/initializers/constants' | ||
27 | |||
28 | const searchPlaylistsRouter = express.Router() | ||
29 | |||
30 | searchPlaylistsRouter.get('/video-playlists', | ||
31 | openapiOperationDoc({ operationId: 'searchPlaylists' }), | ||
32 | paginationValidator, | ||
33 | setDefaultPagination, | ||
34 | videoPlaylistsSearchSortValidator, | ||
35 | setDefaultSearchSort, | ||
36 | optionalAuthenticate, | ||
37 | videoPlaylistsListSearchValidator, | ||
38 | asyncMiddleware(searchVideoPlaylists) | ||
39 | ) | ||
40 | |||
41 | // --------------------------------------------------------------------------- | ||
42 | |||
43 | export { searchPlaylistsRouter } | ||
44 | |||
45 | // --------------------------------------------------------------------------- | ||
46 | |||
47 | function searchVideoPlaylists (req: express.Request, res: express.Response) { | ||
48 | const query: VideoPlaylistsSearchQuery = req.query | ||
49 | const search = query.search | ||
50 | |||
51 | if (isURISearch(search)) return searchVideoPlaylistsURI(search, res) | ||
52 | |||
53 | if (isSearchIndexSearch(query)) { | ||
54 | return searchVideoPlaylistsIndex(query, res) | ||
55 | } | ||
56 | |||
57 | return searchVideoPlaylistsDB(query, res) | ||
58 | } | ||
59 | |||
60 | async function searchVideoPlaylistsIndex (query: VideoPlaylistsSearchQuery, res: express.Response) { | ||
61 | const result = await buildMutedForSearchIndex(res) | ||
62 | |||
63 | const body = await Hooks.wrapObject(Object.assign(query, result), 'filter:api.search.video-playlists.index.list.params') | ||
64 | |||
65 | const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/video-playlists' | ||
66 | |||
67 | try { | ||
68 | logger.debug('Doing video playlists search index request on %s.', url, { body }) | ||
69 | |||
70 | const { body: searchIndexResult } = await doJSONRequest<ResultList<VideoPlaylist>>(url, { method: 'POST', json: body }) | ||
71 | const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.video-playlists.index.list.result') | ||
72 | |||
73 | return res.json(jsonResult) | ||
74 | } catch (err) { | ||
75 | logger.warn('Cannot use search index to make video playlists search.', { err }) | ||
76 | |||
77 | return res.fail({ | ||
78 | status: HttpStatusCode.INTERNAL_SERVER_ERROR_500, | ||
79 | message: 'Cannot use search index to make video playlists search' | ||
80 | }) | ||
81 | } | ||
82 | } | ||
83 | |||
84 | async function searchVideoPlaylistsDB (query: VideoPlaylistsSearchQuery, res: express.Response) { | ||
85 | const serverActor = await getServerActor() | ||
86 | |||
87 | const apiOptions = await Hooks.wrapObject({ | ||
88 | followerActorId: serverActor.id, | ||
89 | search: query.search, | ||
90 | start: query.start, | ||
91 | count: query.count, | ||
92 | sort: query.sort | ||
93 | }, 'filter:api.search.video-playlists.local.list.params') | ||
94 | |||
95 | const resultList = await Hooks.wrapPromiseFun( | ||
96 | VideoPlaylistModel.searchForApi, | ||
97 | apiOptions, | ||
98 | 'filter:api.search.video-playlists.local.list.result' | ||
99 | ) | ||
100 | |||
101 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
102 | } | ||
103 | |||
104 | async function searchVideoPlaylistsURI (search: string, res: express.Response) { | ||
105 | let videoPlaylist: MVideoPlaylistFullSummary | ||
106 | |||
107 | if (isUserAbleToSearchRemoteURI(res)) { | ||
108 | try { | ||
109 | videoPlaylist = await getOrCreateAPVideoPlaylist(search) | ||
110 | } catch (err) { | ||
111 | logger.info('Cannot search remote video playlist %s.', search, { err }) | ||
112 | } | ||
113 | } else { | ||
114 | videoPlaylist = await VideoPlaylistModel.loadByUrlWithAccountAndChannelSummary(sanitizeLocalUrl(search)) | ||
115 | } | ||
116 | |||
117 | return res.json({ | ||
118 | total: videoPlaylist ? 1 : 0, | ||
119 | data: videoPlaylist ? [ videoPlaylist.toFormattedJSON() ] : [] | ||
120 | }) | ||
121 | } | ||
122 | |||
123 | function sanitizeLocalUrl (url: string) { | ||
124 | if (!url) return '' | ||
125 | |||
126 | // Handle alternative channel URLs | ||
127 | return url.replace(new RegExp('^' + WEBSERVER.URL + '/videos/watch/playlist/'), WEBSERVER.URL + '/video-playlists/') | ||
128 | .replace(new RegExp('^' + WEBSERVER.URL + '/w/p/'), WEBSERVER.URL + '/video-playlists/') | ||
129 | } | ||
diff --git a/server/controllers/api/search/search-videos.ts b/server/controllers/api/search/search-videos.ts new file mode 100644 index 000000000..b626baa28 --- /dev/null +++ b/server/controllers/api/search/search-videos.ts | |||
@@ -0,0 +1,153 @@ | |||
1 | import * as express from 'express' | ||
2 | import { sanitizeUrl } from '@server/helpers/core-utils' | ||
3 | import { doJSONRequest } from '@server/helpers/requests' | ||
4 | import { CONFIG } from '@server/initializers/config' | ||
5 | import { WEBSERVER } from '@server/initializers/constants' | ||
6 | import { getOrCreateAPVideo } from '@server/lib/activitypub/videos' | ||
7 | import { Hooks } from '@server/lib/plugins/hooks' | ||
8 | import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search' | ||
9 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | ||
10 | import { ResultList, Video } from '@shared/models' | ||
11 | import { VideosSearchQuery } from '../../../../shared/models/search' | ||
12 | import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils' | ||
13 | import { logger } from '../../../helpers/logger' | ||
14 | import { getFormattedObjects } from '../../../helpers/utils' | ||
15 | import { | ||
16 | asyncMiddleware, | ||
17 | commonVideosFiltersValidator, | ||
18 | openapiOperationDoc, | ||
19 | optionalAuthenticate, | ||
20 | paginationValidator, | ||
21 | setDefaultPagination, | ||
22 | setDefaultSearchSort, | ||
23 | videosSearchSortValidator, | ||
24 | videosSearchValidator | ||
25 | } from '../../../middlewares' | ||
26 | import { VideoModel } from '../../../models/video/video' | ||
27 | import { MVideoAccountLightBlacklistAllFiles } from '../../../types/models' | ||
28 | |||
29 | const searchVideosRouter = express.Router() | ||
30 | |||
31 | searchVideosRouter.get('/videos', | ||
32 | openapiOperationDoc({ operationId: 'searchVideos' }), | ||
33 | paginationValidator, | ||
34 | setDefaultPagination, | ||
35 | videosSearchSortValidator, | ||
36 | setDefaultSearchSort, | ||
37 | optionalAuthenticate, | ||
38 | commonVideosFiltersValidator, | ||
39 | videosSearchValidator, | ||
40 | asyncMiddleware(searchVideos) | ||
41 | ) | ||
42 | |||
43 | // --------------------------------------------------------------------------- | ||
44 | |||
45 | export { searchVideosRouter } | ||
46 | |||
47 | // --------------------------------------------------------------------------- | ||
48 | |||
49 | function searchVideos (req: express.Request, res: express.Response) { | ||
50 | const query: VideosSearchQuery = req.query | ||
51 | const search = query.search | ||
52 | |||
53 | if (isURISearch(search)) { | ||
54 | return searchVideoURI(search, res) | ||
55 | } | ||
56 | |||
57 | if (isSearchIndexSearch(query)) { | ||
58 | return searchVideosIndex(query, res) | ||
59 | } | ||
60 | |||
61 | return searchVideosDB(query, res) | ||
62 | } | ||
63 | |||
64 | async function searchVideosIndex (query: VideosSearchQuery, res: express.Response) { | ||
65 | const result = await buildMutedForSearchIndex(res) | ||
66 | |||
67 | let body: VideosSearchQuery = Object.assign(query, result) | ||
68 | |||
69 | // Use the default instance NSFW policy if not specified | ||
70 | if (!body.nsfw) { | ||
71 | const nsfwPolicy = res.locals.oauth | ||
72 | ? res.locals.oauth.token.User.nsfwPolicy | ||
73 | : CONFIG.INSTANCE.DEFAULT_NSFW_POLICY | ||
74 | |||
75 | body.nsfw = nsfwPolicy === 'do_not_list' | ||
76 | ? 'false' | ||
77 | : 'both' | ||
78 | } | ||
79 | |||
80 | body = await Hooks.wrapObject(body, 'filter:api.search.videos.index.list.params') | ||
81 | |||
82 | const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/videos' | ||
83 | |||
84 | try { | ||
85 | logger.debug('Doing videos search index request on %s.', url, { body }) | ||
86 | |||
87 | const { body: searchIndexResult } = await doJSONRequest<ResultList<Video>>(url, { method: 'POST', json: body }) | ||
88 | const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.videos.index.list.result') | ||
89 | |||
90 | return res.json(jsonResult) | ||
91 | } catch (err) { | ||
92 | logger.warn('Cannot use search index to make video search.', { err }) | ||
93 | |||
94 | return res.fail({ | ||
95 | status: HttpStatusCode.INTERNAL_SERVER_ERROR_500, | ||
96 | message: 'Cannot use search index to make video search' | ||
97 | }) | ||
98 | } | ||
99 | } | ||
100 | |||
101 | async function searchVideosDB (query: VideosSearchQuery, res: express.Response) { | ||
102 | const apiOptions = await Hooks.wrapObject(Object.assign(query, { | ||
103 | includeLocalVideos: true, | ||
104 | nsfw: buildNSFWFilter(res, query.nsfw), | ||
105 | filter: query.filter, | ||
106 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined | ||
107 | }), 'filter:api.search.videos.local.list.params') | ||
108 | |||
109 | const resultList = await Hooks.wrapPromiseFun( | ||
110 | VideoModel.searchAndPopulateAccountAndServer, | ||
111 | apiOptions, | ||
112 | 'filter:api.search.videos.local.list.result' | ||
113 | ) | ||
114 | |||
115 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
116 | } | ||
117 | |||
118 | async function searchVideoURI (url: string, res: express.Response) { | ||
119 | let video: MVideoAccountLightBlacklistAllFiles | ||
120 | |||
121 | // Check if we can fetch a remote video with the URL | ||
122 | if (isUserAbleToSearchRemoteURI(res)) { | ||
123 | try { | ||
124 | const syncParam = { | ||
125 | likes: false, | ||
126 | dislikes: false, | ||
127 | shares: false, | ||
128 | comments: false, | ||
129 | thumbnail: true, | ||
130 | refreshVideo: false | ||
131 | } | ||
132 | |||
133 | const result = await getOrCreateAPVideo({ videoObject: url, syncParam }) | ||
134 | video = result ? result.video : undefined | ||
135 | } catch (err) { | ||
136 | logger.info('Cannot search remote video %s.', url, { err }) | ||
137 | } | ||
138 | } else { | ||
139 | video = await VideoModel.loadByUrlAndPopulateAccount(sanitizeLocalUrl(url)) | ||
140 | } | ||
141 | |||
142 | return res.json({ | ||
143 | total: video ? 1 : 0, | ||
144 | data: video ? [ video.toFormattedJSON() ] : [] | ||
145 | }) | ||
146 | } | ||
147 | |||
148 | function sanitizeLocalUrl (url: string) { | ||
149 | if (!url) return '' | ||
150 | |||
151 | // Handle alternative video URLs | ||
152 | return url.replace(new RegExp('^' + WEBSERVER.URL + '/w/'), WEBSERVER.URL + '/videos/watch/') | ||
153 | } | ||
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts index 03aa918d3..bc8d203b0 100644 --- a/server/controllers/api/video-channel.ts +++ b/server/controllers/api/video-channel.ts | |||
@@ -32,7 +32,7 @@ import { | |||
32 | videoChannelsUpdateValidator, | 32 | videoChannelsUpdateValidator, |
33 | videoPlaylistsSortValidator | 33 | videoPlaylistsSortValidator |
34 | } from '../../middlewares' | 34 | } from '../../middlewares' |
35 | import { videoChannelsNameWithHostValidator, videoChannelsOwnSearchValidator, videosSortValidator } from '../../middlewares/validators' | 35 | import { videoChannelsListValidator, videoChannelsNameWithHostValidator, videosSortValidator } from '../../middlewares/validators' |
36 | import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/actor-image' | 36 | import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/actor-image' |
37 | import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists' | 37 | import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists' |
38 | import { AccountModel } from '../../models/account/account' | 38 | import { AccountModel } from '../../models/account/account' |
@@ -51,7 +51,7 @@ videoChannelRouter.get('/', | |||
51 | videoChannelsSortValidator, | 51 | videoChannelsSortValidator, |
52 | setDefaultSort, | 52 | setDefaultSort, |
53 | setDefaultPagination, | 53 | setDefaultPagination, |
54 | videoChannelsOwnSearchValidator, | 54 | videoChannelsListValidator, |
55 | asyncMiddleware(listVideoChannels) | 55 | asyncMiddleware(listVideoChannels) |
56 | ) | 56 | ) |
57 | 57 | ||
diff --git a/server/controllers/api/video-playlist.ts b/server/controllers/api/video-playlist.ts index 7c816b93a..c25aed20b 100644 --- a/server/controllers/api/video-playlist.ts +++ b/server/controllers/api/video-playlist.ts | |||
@@ -1,7 +1,9 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { join } from 'path' | 2 | import { join } from 'path' |
3 | import { scheduleRefreshIfNeeded } from '@server/lib/activitypub/playlists' | ||
3 | import { getServerActor } from '@server/models/application/application' | 4 | import { getServerActor } from '@server/models/application/application' |
4 | import { MVideoPlaylistFull, MVideoPlaylistThumbnail, MVideoThumbnail } from '@server/types/models' | 5 | import { MVideoPlaylistFull, MVideoPlaylistThumbnail, MVideoThumbnail } from '@server/types/models' |
6 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
5 | import { VideoPlaylistCreate } from '../../../shared/models/videos/playlist/video-playlist-create.model' | 7 | import { VideoPlaylistCreate } from '../../../shared/models/videos/playlist/video-playlist-create.model' |
6 | import { VideoPlaylistElementCreate } from '../../../shared/models/videos/playlist/video-playlist-element-create.model' | 8 | import { VideoPlaylistElementCreate } from '../../../shared/models/videos/playlist/video-playlist-element-create.model' |
7 | import { VideoPlaylistElementUpdate } from '../../../shared/models/videos/playlist/video-playlist-element-update.model' | 9 | import { VideoPlaylistElementUpdate } from '../../../shared/models/videos/playlist/video-playlist-element-update.model' |
@@ -17,7 +19,6 @@ import { MIMETYPES, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers/constant | |||
17 | import { sequelizeTypescript } from '../../initializers/database' | 19 | import { sequelizeTypescript } from '../../initializers/database' |
18 | import { sendCreateVideoPlaylist, sendDeleteVideoPlaylist, sendUpdateVideoPlaylist } from '../../lib/activitypub/send' | 20 | import { sendCreateVideoPlaylist, sendDeleteVideoPlaylist, sendUpdateVideoPlaylist } from '../../lib/activitypub/send' |
19 | import { getLocalVideoPlaylistActivityPubUrl, getLocalVideoPlaylistElementActivityPubUrl } from '../../lib/activitypub/url' | 21 | import { getLocalVideoPlaylistActivityPubUrl, getLocalVideoPlaylistElementActivityPubUrl } from '../../lib/activitypub/url' |
20 | import { JobQueue } from '../../lib/job-queue' | ||
21 | import { updatePlaylistMiniatureFromExisting } from '../../lib/thumbnail' | 22 | import { updatePlaylistMiniatureFromExisting } from '../../lib/thumbnail' |
22 | import { | 23 | import { |
23 | asyncMiddleware, | 24 | asyncMiddleware, |
@@ -42,7 +43,6 @@ import { | |||
42 | import { AccountModel } from '../../models/account/account' | 43 | import { AccountModel } from '../../models/account/account' |
43 | import { VideoPlaylistModel } from '../../models/video/video-playlist' | 44 | import { VideoPlaylistModel } from '../../models/video/video-playlist' |
44 | import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' | 45 | import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' |
45 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
46 | 46 | ||
47 | const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { thumbnailfile: CONFIG.STORAGE.TMP_DIR }) | 47 | const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { thumbnailfile: CONFIG.STORAGE.TMP_DIR }) |
48 | 48 | ||
@@ -144,9 +144,7 @@ async function listVideoPlaylists (req: express.Request, res: express.Response) | |||
144 | function getVideoPlaylist (req: express.Request, res: express.Response) { | 144 | function getVideoPlaylist (req: express.Request, res: express.Response) { |
145 | const videoPlaylist = res.locals.videoPlaylistSummary | 145 | const videoPlaylist = res.locals.videoPlaylistSummary |
146 | 146 | ||
147 | if (videoPlaylist.isOutdated()) { | 147 | scheduleRefreshIfNeeded(videoPlaylist) |
148 | JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video-playlist', url: videoPlaylist.url } }) | ||
149 | } | ||
150 | 148 | ||
151 | return res.json(videoPlaylist.toFormattedJSON()) | 149 | return res.json(videoPlaylist.toFormattedJSON()) |
152 | } | 150 | } |