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 | |
parent | 33eb19e5199cc9fa4d73c6675c97508e3e072ef9 (diff) | |
download | PeerTube-37a44fc915eef2140e22ceb96aba6b6eb2509007.tar.gz PeerTube-37a44fc915eef2140e22ceb96aba6b6eb2509007.tar.zst PeerTube-37a44fc915eef2140e22ceb96aba6b6eb2509007.zip |
Add ability to search playlists
Diffstat (limited to 'server')
42 files changed, 1193 insertions, 373 deletions
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts index bf8e3160b..d7de1b9bd 100644 --- a/server/controllers/activitypub/client.ts +++ b/server/controllers/activitypub/client.ts | |||
@@ -155,7 +155,8 @@ activityPubClientRouter.get('/redundancy/streaming-playlists/:streamingPlaylistT | |||
155 | asyncMiddleware(videoRedundancyController) | 155 | asyncMiddleware(videoRedundancyController) |
156 | ) | 156 | ) |
157 | 157 | ||
158 | activityPubClientRouter.get('/video-playlists/:playlistId', | 158 | activityPubClientRouter.get( |
159 | [ '/video-playlists/:playlistId', '/videos/watch/playlist/:playlistId', '/w/p/:playlistId' ], | ||
159 | executeIfActivityPub, | 160 | executeIfActivityPub, |
160 | asyncMiddleware(videoPlaylistsGetValidator('all')), | 161 | asyncMiddleware(videoPlaylistsGetValidator('all')), |
161 | asyncMiddleware(videoPlaylistController) | 162 | asyncMiddleware(videoPlaylistController) |
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 | } |
diff --git a/server/helpers/custom-validators/activitypub/playlist.ts b/server/helpers/custom-validators/activitypub/playlist.ts index bd0d16a4a..72c5b80e9 100644 --- a/server/helpers/custom-validators/activitypub/playlist.ts +++ b/server/helpers/custom-validators/activitypub/playlist.ts | |||
@@ -1,13 +1,16 @@ | |||
1 | import { exists, isDateValid } from '../misc' | ||
2 | import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' | ||
3 | import validator from 'validator' | 1 | import validator from 'validator' |
4 | import { PlaylistElementObject } from '../../../../shared/models/activitypub/objects/playlist-element-object' | 2 | import { PlaylistElementObject } from '../../../../shared/models/activitypub/objects/playlist-element-object' |
3 | import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' | ||
4 | import { exists, isDateValid, isUUIDValid } from '../misc' | ||
5 | import { isVideoPlaylistNameValid } from '../video-playlists' | ||
5 | import { isActivityPubUrlValid } from './misc' | 6 | import { isActivityPubUrlValid } from './misc' |
6 | 7 | ||
7 | function isPlaylistObjectValid (object: PlaylistObject) { | 8 | function isPlaylistObjectValid (object: PlaylistObject) { |
8 | return exists(object) && | 9 | return exists(object) && |
9 | object.type === 'Playlist' && | 10 | object.type === 'Playlist' && |
10 | validator.isInt(object.totalItems + '') && | 11 | validator.isInt(object.totalItems + '') && |
12 | isVideoPlaylistNameValid(object.name) && | ||
13 | isUUIDValid(object.uuid) && | ||
11 | isDateValid(object.published) && | 14 | isDateValid(object.published) && |
12 | isDateValid(object.updated) | 15 | isDateValid(object.updated) |
13 | } | 16 | } |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 06b4e5a18..cd00b73d5 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -77,6 +77,7 @@ const SORTABLE_COLUMNS = { | |||
77 | // Don't forget to update peertube-search-index with the same values | 77 | // Don't forget to update peertube-search-index with the same values |
78 | VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'match' ], | 78 | VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'match' ], |
79 | VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ], | 79 | VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ], |
80 | VIDEO_PLAYLISTS_SEARCH: [ 'match', 'displayName', 'createdAt' ], | ||
80 | 81 | ||
81 | ABUSES: [ 'id', 'createdAt', 'state' ], | 82 | ABUSES: [ 'id', 'createdAt', 'state' ], |
82 | 83 | ||
diff --git a/server/lib/activitypub/actors/get.ts b/server/lib/activitypub/actors/get.ts index d7cf2b678..8681ea02a 100644 --- a/server/lib/activitypub/actors/get.ts +++ b/server/lib/activitypub/actors/get.ts | |||
@@ -116,7 +116,7 @@ async function scheduleOutboxFetchIfNeeded (actor: MActor, created: boolean, ref | |||
116 | async function schedulePlaylistFetchIfNeeded (actor: MActorAccountId, created: boolean, accountPlaylistsUrl: string) { | 116 | async function schedulePlaylistFetchIfNeeded (actor: MActorAccountId, created: boolean, accountPlaylistsUrl: string) { |
117 | // We created a new account: fetch the playlists | 117 | // We created a new account: fetch the playlists |
118 | if (created === true && actor.Account && accountPlaylistsUrl) { | 118 | if (created === true && actor.Account && accountPlaylistsUrl) { |
119 | const payload = { uri: accountPlaylistsUrl, accountId: actor.Account.id, type: 'account-playlists' as 'account-playlists' } | 119 | const payload = { uri: accountPlaylistsUrl, type: 'account-playlists' as 'account-playlists' } |
120 | await JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload }) | 120 | await JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload }) |
121 | } | 121 | } |
122 | } | 122 | } |
diff --git a/server/lib/activitypub/playlists/create-update.ts b/server/lib/activitypub/playlists/create-update.ts index 37d748de4..ea3e61ac5 100644 --- a/server/lib/activitypub/playlists/create-update.ts +++ b/server/lib/activitypub/playlists/create-update.ts | |||
@@ -1,3 +1,5 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import { getAPId } from '@server/helpers/activitypub' | ||
1 | import { isArray } from '@server/helpers/custom-validators/misc' | 3 | import { isArray } from '@server/helpers/custom-validators/misc' |
2 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | 4 | import { logger, loggerTagsFactory } from '@server/helpers/logger' |
3 | import { CRAWL_REQUEST_CONCURRENCY } from '@server/initializers/constants' | 5 | import { CRAWL_REQUEST_CONCURRENCY } from '@server/initializers/constants' |
@@ -6,7 +8,7 @@ import { updatePlaylistMiniatureFromUrl } from '@server/lib/thumbnail' | |||
6 | import { VideoPlaylistModel } from '@server/models/video/video-playlist' | 8 | import { VideoPlaylistModel } from '@server/models/video/video-playlist' |
7 | import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element' | 9 | import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element' |
8 | import { FilteredModelAttributes } from '@server/types' | 10 | import { FilteredModelAttributes } from '@server/types' |
9 | import { MAccountDefault, MAccountId, MThumbnail, MVideoPlaylist, MVideoPlaylistFull } from '@server/types/models' | 11 | import { MThumbnail, MVideoPlaylist, MVideoPlaylistFull, MVideoPlaylistVideosLength } from '@server/types/models' |
10 | import { AttributesOnly } from '@shared/core-utils' | 12 | import { AttributesOnly } from '@shared/core-utils' |
11 | import { PlaylistObject } from '@shared/models' | 13 | import { PlaylistObject } from '@shared/models' |
12 | import { getOrCreateAPActor } from '../actors' | 14 | import { getOrCreateAPActor } from '../actors' |
@@ -19,11 +21,9 @@ import { | |||
19 | playlistObjectToDBAttributes | 21 | playlistObjectToDBAttributes |
20 | } from './shared' | 22 | } from './shared' |
21 | 23 | ||
22 | import Bluebird = require('bluebird') | ||
23 | |||
24 | const lTags = loggerTagsFactory('ap', 'video-playlist') | 24 | const lTags = loggerTagsFactory('ap', 'video-playlist') |
25 | 25 | ||
26 | async function createAccountPlaylists (playlistUrls: string[], account: MAccountDefault) { | 26 | async function createAccountPlaylists (playlistUrls: string[]) { |
27 | await Bluebird.map(playlistUrls, async playlistUrl => { | 27 | await Bluebird.map(playlistUrls, async playlistUrl => { |
28 | try { | 28 | try { |
29 | const exists = await VideoPlaylistModel.doesPlaylistExist(playlistUrl) | 29 | const exists = await VideoPlaylistModel.doesPlaylistExist(playlistUrl) |
@@ -35,19 +35,19 @@ async function createAccountPlaylists (playlistUrls: string[], account: MAccount | |||
35 | throw new Error(`Cannot refresh remote playlist ${playlistUrl}: invalid body.`) | 35 | throw new Error(`Cannot refresh remote playlist ${playlistUrl}: invalid body.`) |
36 | } | 36 | } |
37 | 37 | ||
38 | return createOrUpdateVideoPlaylist(playlistObject, account, playlistObject.to) | 38 | return createOrUpdateVideoPlaylist(playlistObject) |
39 | } catch (err) { | 39 | } catch (err) { |
40 | logger.warn('Cannot add playlist element %s.', playlistUrl, { err, ...lTags(playlistUrl) }) | 40 | logger.warn('Cannot add playlist element %s.', playlistUrl, { err, ...lTags(playlistUrl) }) |
41 | } | 41 | } |
42 | }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) | 42 | }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) |
43 | } | 43 | } |
44 | 44 | ||
45 | async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) { | 45 | async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, to?: string[]) { |
46 | const playlistAttributes = playlistObjectToDBAttributes(playlistObject, byAccount, to) | 46 | const playlistAttributes = playlistObjectToDBAttributes(playlistObject, to || playlistObject.to) |
47 | 47 | ||
48 | await setVideoChannelIfNeeded(playlistObject, playlistAttributes) | 48 | await setVideoChannel(playlistObject, playlistAttributes) |
49 | 49 | ||
50 | const [ upsertPlaylist ] = await VideoPlaylistModel.upsert<MVideoPlaylist>(playlistAttributes, { returning: true }) | 50 | const [ upsertPlaylist ] = await VideoPlaylistModel.upsert<MVideoPlaylistVideosLength>(playlistAttributes, { returning: true }) |
51 | 51 | ||
52 | const playlistElementUrls = await fetchElementUrls(playlistObject) | 52 | const playlistElementUrls = await fetchElementUrls(playlistObject) |
53 | 53 | ||
@@ -56,7 +56,10 @@ async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAc | |||
56 | 56 | ||
57 | await updatePlaylistThumbnail(playlistObject, playlist) | 57 | await updatePlaylistThumbnail(playlistObject, playlist) |
58 | 58 | ||
59 | return rebuildVideoPlaylistElements(playlistElementUrls, playlist) | 59 | const elementsLength = await rebuildVideoPlaylistElements(playlistElementUrls, playlist) |
60 | playlist.setVideosLength(elementsLength) | ||
61 | |||
62 | return playlist | ||
60 | } | 63 | } |
61 | 64 | ||
62 | // --------------------------------------------------------------------------- | 65 | // --------------------------------------------------------------------------- |
@@ -68,10 +71,12 @@ export { | |||
68 | 71 | ||
69 | // --------------------------------------------------------------------------- | 72 | // --------------------------------------------------------------------------- |
70 | 73 | ||
71 | async function setVideoChannelIfNeeded (playlistObject: PlaylistObject, playlistAttributes: AttributesOnly<VideoPlaylistModel>) { | 74 | async function setVideoChannel (playlistObject: PlaylistObject, playlistAttributes: AttributesOnly<VideoPlaylistModel>) { |
72 | if (!isArray(playlistObject.attributedTo) || playlistObject.attributedTo.length !== 1) return | 75 | if (!isArray(playlistObject.attributedTo) || playlistObject.attributedTo.length !== 1) { |
76 | throw new Error('Not attributed to for playlist object ' + getAPId(playlistObject)) | ||
77 | } | ||
73 | 78 | ||
74 | const actor = await getOrCreateAPActor(playlistObject.attributedTo[0]) | 79 | const actor = await getOrCreateAPActor(playlistObject.attributedTo[0], 'all') |
75 | 80 | ||
76 | if (!actor.VideoChannel) { | 81 | if (!actor.VideoChannel) { |
77 | logger.warn('Playlist "attributedTo" %s is not a video channel.', playlistObject.id, { playlistObject, ...lTags(playlistObject.id) }) | 82 | logger.warn('Playlist "attributedTo" %s is not a video channel.', playlistObject.id, { playlistObject, ...lTags(playlistObject.id) }) |
@@ -79,6 +84,7 @@ async function setVideoChannelIfNeeded (playlistObject: PlaylistObject, playlist | |||
79 | } | 84 | } |
80 | 85 | ||
81 | playlistAttributes.videoChannelId = actor.VideoChannel.id | 86 | playlistAttributes.videoChannelId = actor.VideoChannel.id |
87 | playlistAttributes.ownerAccountId = actor.VideoChannel.Account.id | ||
82 | } | 88 | } |
83 | 89 | ||
84 | async function fetchElementUrls (playlistObject: PlaylistObject) { | 90 | async function fetchElementUrls (playlistObject: PlaylistObject) { |
@@ -128,7 +134,7 @@ async function rebuildVideoPlaylistElements (elementUrls: string[], playlist: MV | |||
128 | 134 | ||
129 | logger.info('Rebuilt playlist %s with %s elements.', playlist.url, elementsToCreate.length, lTags(playlist.uuid, playlist.url)) | 135 | logger.info('Rebuilt playlist %s with %s elements.', playlist.url, elementsToCreate.length, lTags(playlist.uuid, playlist.url)) |
130 | 136 | ||
131 | return undefined | 137 | return elementsToCreate.length |
132 | } | 138 | } |
133 | 139 | ||
134 | async function buildElementsDBAttributes (elementUrls: string[], playlist: MVideoPlaylist) { | 140 | async function buildElementsDBAttributes (elementUrls: string[], playlist: MVideoPlaylist) { |
diff --git a/server/lib/activitypub/playlists/get.ts b/server/lib/activitypub/playlists/get.ts new file mode 100644 index 000000000..2c19c503a --- /dev/null +++ b/server/lib/activitypub/playlists/get.ts | |||
@@ -0,0 +1,35 @@ | |||
1 | import { getAPId } from '@server/helpers/activitypub' | ||
2 | import { VideoPlaylistModel } from '@server/models/video/video-playlist' | ||
3 | import { MVideoPlaylistFullSummary } from '@server/types/models' | ||
4 | import { APObject } from '@shared/models' | ||
5 | import { createOrUpdateVideoPlaylist } from './create-update' | ||
6 | import { scheduleRefreshIfNeeded } from './refresh' | ||
7 | import { fetchRemoteVideoPlaylist } from './shared' | ||
8 | |||
9 | async function getOrCreateAPVideoPlaylist (playlistObjectArg: APObject): Promise<MVideoPlaylistFullSummary> { | ||
10 | const playlistUrl = getAPId(playlistObjectArg) | ||
11 | |||
12 | const playlistFromDatabase = await VideoPlaylistModel.loadByUrlWithAccountAndChannelSummary(playlistUrl) | ||
13 | |||
14 | if (playlistFromDatabase) { | ||
15 | scheduleRefreshIfNeeded(playlistFromDatabase) | ||
16 | |||
17 | return playlistFromDatabase | ||
18 | } | ||
19 | |||
20 | const { playlistObject } = await fetchRemoteVideoPlaylist(playlistUrl) | ||
21 | if (!playlistObject) throw new Error('Cannot fetch remote playlist with url: ' + playlistUrl) | ||
22 | |||
23 | // playlistUrl is just an alias/rediraction, so process object id instead | ||
24 | if (playlistObject.id !== playlistUrl) return getOrCreateAPVideoPlaylist(playlistObject) | ||
25 | |||
26 | const playlistCreated = await createOrUpdateVideoPlaylist(playlistObject) | ||
27 | |||
28 | return playlistCreated | ||
29 | } | ||
30 | |||
31 | // --------------------------------------------------------------------------- | ||
32 | |||
33 | export { | ||
34 | getOrCreateAPVideoPlaylist | ||
35 | } | ||
diff --git a/server/lib/activitypub/playlists/index.ts b/server/lib/activitypub/playlists/index.ts index 2885830b4..e2470a674 100644 --- a/server/lib/activitypub/playlists/index.ts +++ b/server/lib/activitypub/playlists/index.ts | |||
@@ -1,2 +1,3 @@ | |||
1 | export * from './get' | ||
1 | export * from './create-update' | 2 | export * from './create-update' |
2 | export * from './refresh' | 3 | export * from './refresh' |
diff --git a/server/lib/activitypub/playlists/refresh.ts b/server/lib/activitypub/playlists/refresh.ts index 6f3a6be37..ef3cb3fe4 100644 --- a/server/lib/activitypub/playlists/refresh.ts +++ b/server/lib/activitypub/playlists/refresh.ts | |||
@@ -1,10 +1,17 @@ | |||
1 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | 1 | import { logger, loggerTagsFactory } from '@server/helpers/logger' |
2 | import { PeerTubeRequestError } from '@server/helpers/requests' | 2 | import { PeerTubeRequestError } from '@server/helpers/requests' |
3 | import { MVideoPlaylistOwner } from '@server/types/models' | 3 | import { JobQueue } from '@server/lib/job-queue' |
4 | import { MVideoPlaylist, MVideoPlaylistOwner } from '@server/types/models' | ||
4 | import { HttpStatusCode } from '@shared/core-utils' | 5 | import { HttpStatusCode } from '@shared/core-utils' |
5 | import { createOrUpdateVideoPlaylist } from './create-update' | 6 | import { createOrUpdateVideoPlaylist } from './create-update' |
6 | import { fetchRemoteVideoPlaylist } from './shared' | 7 | import { fetchRemoteVideoPlaylist } from './shared' |
7 | 8 | ||
9 | function scheduleRefreshIfNeeded (playlist: MVideoPlaylist) { | ||
10 | if (!playlist.isOutdated()) return | ||
11 | |||
12 | JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video-playlist', url: playlist.url } }) | ||
13 | } | ||
14 | |||
8 | async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner): Promise<MVideoPlaylistOwner> { | 15 | async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner): Promise<MVideoPlaylistOwner> { |
9 | if (!videoPlaylist.isOutdated()) return videoPlaylist | 16 | if (!videoPlaylist.isOutdated()) return videoPlaylist |
10 | 17 | ||
@@ -22,8 +29,7 @@ async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner) | |||
22 | return videoPlaylist | 29 | return videoPlaylist |
23 | } | 30 | } |
24 | 31 | ||
25 | const byAccount = videoPlaylist.OwnerAccount | 32 | await createOrUpdateVideoPlaylist(playlistObject) |
26 | await createOrUpdateVideoPlaylist(playlistObject, byAccount, playlistObject.to) | ||
27 | 33 | ||
28 | return videoPlaylist | 34 | return videoPlaylist |
29 | } catch (err) { | 35 | } catch (err) { |
@@ -42,5 +48,6 @@ async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner) | |||
42 | } | 48 | } |
43 | 49 | ||
44 | export { | 50 | export { |
51 | scheduleRefreshIfNeeded, | ||
45 | refreshVideoPlaylistIfNeeded | 52 | refreshVideoPlaylistIfNeeded |
46 | } | 53 | } |
diff --git a/server/lib/activitypub/playlists/shared/object-to-model-attributes.ts b/server/lib/activitypub/playlists/shared/object-to-model-attributes.ts index 6ec44485e..70fd335bc 100644 --- a/server/lib/activitypub/playlists/shared/object-to-model-attributes.ts +++ b/server/lib/activitypub/playlists/shared/object-to-model-attributes.ts | |||
@@ -1,11 +1,11 @@ | |||
1 | import { ACTIVITY_PUB } from '@server/initializers/constants' | 1 | import { ACTIVITY_PUB } from '@server/initializers/constants' |
2 | import { VideoPlaylistModel } from '@server/models/video/video-playlist' | 2 | import { VideoPlaylistModel } from '@server/models/video/video-playlist' |
3 | import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element' | 3 | import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element' |
4 | import { MAccountId, MVideoId, MVideoPlaylistId } from '@server/types/models' | 4 | import { MVideoId, MVideoPlaylistId } from '@server/types/models' |
5 | import { AttributesOnly } from '@shared/core-utils' | 5 | import { AttributesOnly } from '@shared/core-utils' |
6 | import { PlaylistElementObject, PlaylistObject, VideoPlaylistPrivacy } from '@shared/models' | 6 | import { PlaylistElementObject, PlaylistObject, VideoPlaylistPrivacy } from '@shared/models' |
7 | 7 | ||
8 | function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) { | 8 | function playlistObjectToDBAttributes (playlistObject: PlaylistObject, to: string[]) { |
9 | const privacy = to.includes(ACTIVITY_PUB.PUBLIC) | 9 | const privacy = to.includes(ACTIVITY_PUB.PUBLIC) |
10 | ? VideoPlaylistPrivacy.PUBLIC | 10 | ? VideoPlaylistPrivacy.PUBLIC |
11 | : VideoPlaylistPrivacy.UNLISTED | 11 | : VideoPlaylistPrivacy.UNLISTED |
@@ -16,7 +16,7 @@ function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount | |||
16 | privacy, | 16 | privacy, |
17 | url: playlistObject.id, | 17 | url: playlistObject.id, |
18 | uuid: playlistObject.uuid, | 18 | uuid: playlistObject.uuid, |
19 | ownerAccountId: byAccount.id, | 19 | ownerAccountId: null, |
20 | videoChannelId: null, | 20 | videoChannelId: null, |
21 | createdAt: new Date(playlistObject.published), | 21 | createdAt: new Date(playlistObject.published), |
22 | updatedAt: new Date(playlistObject.updated) | 22 | updatedAt: new Date(playlistObject.updated) |
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts index 6b7f5aae8..70e048d6e 100644 --- a/server/lib/activitypub/process/process-create.ts +++ b/server/lib/activitypub/process/process-create.ts | |||
@@ -128,5 +128,5 @@ async function processCreatePlaylist (activity: ActivityCreate, byActor: MActorS | |||
128 | 128 | ||
129 | if (!byAccount) throw new Error('Cannot create video playlist with the non account actor ' + byActor.url) | 129 | if (!byAccount) throw new Error('Cannot create video playlist with the non account actor ' + byActor.url) |
130 | 130 | ||
131 | await createOrUpdateVideoPlaylist(playlistObject, byAccount, activity.to) | 131 | await createOrUpdateVideoPlaylist(playlistObject, activity.to) |
132 | } | 132 | } |
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts index aa80d5d09..f40008a6b 100644 --- a/server/lib/activitypub/process/process-update.ts +++ b/server/lib/activitypub/process/process-update.ts | |||
@@ -111,5 +111,5 @@ async function processUpdatePlaylist (byActor: MActorSignature, activity: Activi | |||
111 | 111 | ||
112 | if (!byAccount) throw new Error('Cannot update video playlist with the non account actor ' + byActor.url) | 112 | if (!byAccount) throw new Error('Cannot update video playlist with the non account actor ' + byActor.url) |
113 | 113 | ||
114 | await createOrUpdateVideoPlaylist(playlistObject, byAccount, activity.to) | 114 | await createOrUpdateVideoPlaylist(playlistObject, activity.to) |
115 | } | 115 | } |
diff --git a/server/lib/activitypub/videos/get.ts b/server/lib/activitypub/videos/get.ts index 7bb14adc4..f3e2f0625 100644 --- a/server/lib/activitypub/videos/get.ts +++ b/server/lib/activitypub/videos/get.ts | |||
@@ -3,6 +3,7 @@ import { retryTransactionWrapper } from '@server/helpers/database-utils' | |||
3 | import { JobQueue } from '@server/lib/job-queue' | 3 | import { JobQueue } from '@server/lib/job-queue' |
4 | import { loadVideoByUrl, VideoLoadByUrlType } from '@server/lib/model-loaders' | 4 | import { loadVideoByUrl, VideoLoadByUrlType } from '@server/lib/model-loaders' |
5 | import { MVideoAccountLightBlacklistAllFiles, MVideoImmutable, MVideoThumbnail } from '@server/types/models' | 5 | import { MVideoAccountLightBlacklistAllFiles, MVideoImmutable, MVideoThumbnail } from '@server/types/models' |
6 | import { APObject } from '@shared/models' | ||
6 | import { refreshVideoIfNeeded } from './refresh' | 7 | import { refreshVideoIfNeeded } from './refresh' |
7 | import { APVideoCreator, fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared' | 8 | import { APVideoCreator, fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared' |
8 | 9 | ||
@@ -13,21 +14,21 @@ type GetVideoResult <T> = Promise<{ | |||
13 | }> | 14 | }> |
14 | 15 | ||
15 | type GetVideoParamAll = { | 16 | type GetVideoParamAll = { |
16 | videoObject: { id: string } | string | 17 | videoObject: APObject |
17 | syncParam?: SyncParam | 18 | syncParam?: SyncParam |
18 | fetchType?: 'all' | 19 | fetchType?: 'all' |
19 | allowRefresh?: boolean | 20 | allowRefresh?: boolean |
20 | } | 21 | } |
21 | 22 | ||
22 | type GetVideoParamImmutable = { | 23 | type GetVideoParamImmutable = { |
23 | videoObject: { id: string } | string | 24 | videoObject: APObject |
24 | syncParam?: SyncParam | 25 | syncParam?: SyncParam |
25 | fetchType: 'only-immutable-attributes' | 26 | fetchType: 'only-immutable-attributes' |
26 | allowRefresh: false | 27 | allowRefresh: false |
27 | } | 28 | } |
28 | 29 | ||
29 | type GetVideoParamOther = { | 30 | type GetVideoParamOther = { |
30 | videoObject: { id: string } | string | 31 | videoObject: APObject |
31 | syncParam?: SyncParam | 32 | syncParam?: SyncParam |
32 | fetchType?: 'all' | 'only-video' | 33 | fetchType?: 'all' | 'only-video' |
33 | allowRefresh?: boolean | 34 | allowRefresh?: boolean |
diff --git a/server/lib/job-queue/handlers/activitypub-http-fetcher.ts b/server/lib/job-queue/handlers/activitypub-http-fetcher.ts index 04b25f955..ab9675cae 100644 --- a/server/lib/job-queue/handlers/activitypub-http-fetcher.ts +++ b/server/lib/job-queue/handlers/activitypub-http-fetcher.ts | |||
@@ -1,12 +1,11 @@ | |||
1 | import * as Bull from 'bull' | 1 | import * as Bull from 'bull' |
2 | import { ActivitypubHttpFetcherPayload, FetchType } from '@shared/models' | 2 | import { ActivitypubHttpFetcherPayload, FetchType } from '@shared/models' |
3 | import { logger } from '../../../helpers/logger' | 3 | import { logger } from '../../../helpers/logger' |
4 | import { AccountModel } from '../../../models/account/account' | ||
5 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | 4 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' |
6 | import { VideoModel } from '../../../models/video/video' | 5 | import { VideoModel } from '../../../models/video/video' |
7 | import { VideoCommentModel } from '../../../models/video/video-comment' | 6 | import { VideoCommentModel } from '../../../models/video/video-comment' |
8 | import { VideoShareModel } from '../../../models/video/video-share' | 7 | import { VideoShareModel } from '../../../models/video/video-share' |
9 | import { MAccountDefault, MVideoFullLight } from '../../../types/models' | 8 | import { MVideoFullLight } from '../../../types/models' |
10 | import { crawlCollectionPage } from '../../activitypub/crawl' | 9 | import { crawlCollectionPage } from '../../activitypub/crawl' |
11 | import { createAccountPlaylists } from '../../activitypub/playlists' | 10 | import { createAccountPlaylists } from '../../activitypub/playlists' |
12 | import { processActivities } from '../../activitypub/process' | 11 | import { processActivities } from '../../activitypub/process' |
@@ -22,16 +21,13 @@ async function processActivityPubHttpFetcher (job: Bull.Job) { | |||
22 | let video: MVideoFullLight | 21 | let video: MVideoFullLight |
23 | if (payload.videoId) video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId) | 22 | if (payload.videoId) video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId) |
24 | 23 | ||
25 | let account: MAccountDefault | ||
26 | if (payload.accountId) account = await AccountModel.load(payload.accountId) | ||
27 | |||
28 | const fetcherType: { [ id in FetchType ]: (items: any[]) => Promise<any> } = { | 24 | const fetcherType: { [ id in FetchType ]: (items: any[]) => Promise<any> } = { |
29 | 'activity': items => processActivities(items, { outboxUrl: payload.uri, fromFetch: true }), | 25 | 'activity': items => processActivities(items, { outboxUrl: payload.uri, fromFetch: true }), |
30 | 'video-likes': items => createRates(items, video, 'like'), | 26 | 'video-likes': items => createRates(items, video, 'like'), |
31 | 'video-dislikes': items => createRates(items, video, 'dislike'), | 27 | 'video-dislikes': items => createRates(items, video, 'dislike'), |
32 | 'video-shares': items => addVideoShares(items, video), | 28 | 'video-shares': items => addVideoShares(items, video), |
33 | 'video-comments': items => addVideoComments(items), | 29 | 'video-comments': items => addVideoComments(items), |
34 | 'account-playlists': items => createAccountPlaylists(items, account) | 30 | 'account-playlists': items => createAccountPlaylists(items) |
35 | } | 31 | } |
36 | 32 | ||
37 | const cleanerType: { [ id in FetchType ]?: (crawlStartDate: Date) => Promise<any> } = { | 33 | const cleanerType: { [ id in FetchType ]?: (crawlStartDate: Date) => Promise<any> } = { |
diff --git a/server/lib/search.ts b/server/lib/search.ts new file mode 100644 index 000000000..b643a4055 --- /dev/null +++ b/server/lib/search.ts | |||
@@ -0,0 +1,50 @@ | |||
1 | import * as express from 'express' | ||
2 | import { CONFIG } from '@server/initializers/config' | ||
3 | import { AccountBlocklistModel } from '@server/models/account/account-blocklist' | ||
4 | import { getServerActor } from '@server/models/application/application' | ||
5 | import { ServerBlocklistModel } from '@server/models/server/server-blocklist' | ||
6 | import { SearchTargetQuery } from '@shared/models' | ||
7 | |||
8 | function isSearchIndexSearch (query: SearchTargetQuery) { | ||
9 | if (query.searchTarget === 'search-index') return true | ||
10 | |||
11 | const searchIndexConfig = CONFIG.SEARCH.SEARCH_INDEX | ||
12 | |||
13 | if (searchIndexConfig.ENABLED !== true) return false | ||
14 | |||
15 | if (searchIndexConfig.DISABLE_LOCAL_SEARCH) return true | ||
16 | if (searchIndexConfig.IS_DEFAULT_SEARCH && !query.searchTarget) return true | ||
17 | |||
18 | return false | ||
19 | } | ||
20 | |||
21 | async function buildMutedForSearchIndex (res: express.Response) { | ||
22 | const serverActor = await getServerActor() | ||
23 | const accountIds = [ serverActor.Account.id ] | ||
24 | |||
25 | if (res.locals.oauth) { | ||
26 | accountIds.push(res.locals.oauth.token.User.Account.id) | ||
27 | } | ||
28 | |||
29 | const [ blockedHosts, blockedAccounts ] = await Promise.all([ | ||
30 | ServerBlocklistModel.listHostsBlockedBy(accountIds), | ||
31 | AccountBlocklistModel.listHandlesBlockedBy(accountIds) | ||
32 | ]) | ||
33 | |||
34 | return { | ||
35 | blockedHosts, | ||
36 | blockedAccounts | ||
37 | } | ||
38 | } | ||
39 | |||
40 | function isURISearch (search: string) { | ||
41 | if (!search) return false | ||
42 | |||
43 | return search.startsWith('http://') || search.startsWith('https://') | ||
44 | } | ||
45 | |||
46 | export { | ||
47 | isSearchIndexSearch, | ||
48 | buildMutedForSearchIndex, | ||
49 | isURISearch | ||
50 | } | ||
diff --git a/server/middlewares/validators/search.ts b/server/middlewares/validators/search.ts index e2e1c6aae..7bbf81048 100644 --- a/server/middlewares/validators/search.ts +++ b/server/middlewares/validators/search.ts | |||
@@ -49,11 +49,12 @@ const videoChannelsListSearchValidator = [ | |||
49 | } | 49 | } |
50 | ] | 50 | ] |
51 | 51 | ||
52 | const videoChannelsOwnSearchValidator = [ | 52 | const videoPlaylistsListSearchValidator = [ |
53 | query('search').optional().not().isEmpty().withMessage('Should have a valid search'), | 53 | query('search').not().isEmpty().withMessage('Should have a valid search'), |
54 | query('searchTarget').optional().custom(isSearchTargetValid).withMessage('Should have a valid search target'), | ||
54 | 55 | ||
55 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | 56 | (req: express.Request, res: express.Response, next: express.NextFunction) => { |
56 | logger.debug('Checking video channels search query', { parameters: req.query }) | 57 | logger.debug('Checking video playlists search query', { parameters: req.query }) |
57 | 58 | ||
58 | if (areValidationErrors(req, res)) return | 59 | if (areValidationErrors(req, res)) return |
59 | 60 | ||
@@ -66,5 +67,5 @@ const videoChannelsOwnSearchValidator = [ | |||
66 | export { | 67 | export { |
67 | videosSearchValidator, | 68 | videosSearchValidator, |
68 | videoChannelsListSearchValidator, | 69 | videoChannelsListSearchValidator, |
69 | videoChannelsOwnSearchValidator | 70 | videoPlaylistsListSearchValidator |
70 | } | 71 | } |
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts index d67b6f3ba..473010460 100644 --- a/server/middlewares/validators/sort.ts +++ b/server/middlewares/validators/sort.ts | |||
@@ -9,6 +9,7 @@ const SORTABLE_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ABUSES) | |||
9 | const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS) | 9 | const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS) |
10 | const SORTABLE_VIDEOS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS_SEARCH) | 10 | const SORTABLE_VIDEOS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS_SEARCH) |
11 | const SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH) | 11 | const SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH) |
12 | const SORTABLE_VIDEO_PLAYLISTS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_PLAYLISTS_SEARCH) | ||
12 | const SORTABLE_VIDEO_IMPORTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_IMPORTS) | 13 | const SORTABLE_VIDEO_IMPORTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_IMPORTS) |
13 | const SORTABLE_VIDEO_COMMENTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS) | 14 | const SORTABLE_VIDEO_COMMENTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS) |
14 | const SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS) | 15 | const SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS) |
@@ -34,6 +35,7 @@ const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) | |||
34 | const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS) | 35 | const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS) |
35 | const videosSearchSortValidator = checkSort(SORTABLE_VIDEOS_SEARCH_COLUMNS) | 36 | const videosSearchSortValidator = checkSort(SORTABLE_VIDEOS_SEARCH_COLUMNS) |
36 | const videoChannelsSearchSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS) | 37 | const videoChannelsSearchSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS) |
38 | const videoPlaylistsSearchSortValidator = checkSort(SORTABLE_VIDEO_PLAYLISTS_SEARCH_COLUMNS) | ||
37 | const videoCommentsValidator = checkSort(SORTABLE_VIDEO_COMMENTS_COLUMNS) | 39 | const videoCommentsValidator = checkSort(SORTABLE_VIDEO_COMMENTS_COLUMNS) |
38 | const videoCommentThreadsSortValidator = checkSort(SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS) | 40 | const videoCommentThreadsSortValidator = checkSort(SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS) |
39 | const videoRatesSortValidator = checkSort(SORTABLE_VIDEO_RATES_COLUMNS) | 41 | const videoRatesSortValidator = checkSort(SORTABLE_VIDEO_RATES_COLUMNS) |
@@ -75,5 +77,6 @@ export { | |||
75 | userNotificationsSortValidator, | 77 | userNotificationsSortValidator, |
76 | videoPlaylistsSortValidator, | 78 | videoPlaylistsSortValidator, |
77 | videoRedundanciesSortValidator, | 79 | videoRedundanciesSortValidator, |
80 | videoPlaylistsSearchSortValidator, | ||
78 | pluginsSortValidator | 81 | pluginsSortValidator |
79 | } | 82 | } |
diff --git a/server/middlewares/validators/videos/video-channels.ts b/server/middlewares/validators/videos/video-channels.ts index 911a25bfb..e7df185e4 100644 --- a/server/middlewares/validators/videos/video-channels.ts +++ b/server/middlewares/validators/videos/video-channels.ts | |||
@@ -141,6 +141,18 @@ const videoChannelStatsValidator = [ | |||
141 | } | 141 | } |
142 | ] | 142 | ] |
143 | 143 | ||
144 | const videoChannelsListValidator = [ | ||
145 | query('search').optional().not().isEmpty().withMessage('Should have a valid search'), | ||
146 | |||
147 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
148 | logger.debug('Checking video channels search query', { parameters: req.query }) | ||
149 | |||
150 | if (areValidationErrors(req, res)) return | ||
151 | |||
152 | return next() | ||
153 | } | ||
154 | ] | ||
155 | |||
144 | // --------------------------------------------------------------------------- | 156 | // --------------------------------------------------------------------------- |
145 | 157 | ||
146 | export { | 158 | export { |
@@ -148,6 +160,7 @@ export { | |||
148 | videoChannelsUpdateValidator, | 160 | videoChannelsUpdateValidator, |
149 | videoChannelsRemoveValidator, | 161 | videoChannelsRemoveValidator, |
150 | videoChannelsNameWithHostValidator, | 162 | videoChannelsNameWithHostValidator, |
163 | videoChannelsListValidator, | ||
151 | localVideoChannelValidator, | 164 | localVideoChannelValidator, |
152 | videoChannelStatsValidator | 165 | videoChannelStatsValidator |
153 | } | 166 | } |
diff --git a/server/models/video/sql/shared/abstract-videos-query-builder.ts b/server/models/video/sql/shared/abstract-videos-query-builder.ts index 3a1ee5b1f..09776bcb0 100644 --- a/server/models/video/sql/shared/abstract-videos-query-builder.ts +++ b/server/models/video/sql/shared/abstract-videos-query-builder.ts | |||
@@ -18,7 +18,7 @@ export class AbstractVideosQueryBuilder { | |||
18 | logging: options.logging, | 18 | logging: options.logging, |
19 | replacements: this.replacements, | 19 | replacements: this.replacements, |
20 | type: QueryTypes.SELECT as QueryTypes.SELECT, | 20 | type: QueryTypes.SELECT as QueryTypes.SELECT, |
21 | next: false | 21 | nest: false |
22 | } | 22 | } |
23 | 23 | ||
24 | return this.sequelize.query<any>(this.query, queryOptions) | 24 | return this.sequelize.query<any>(this.query, queryOptions) |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 33749ea70..f84b85290 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -434,8 +434,8 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
434 | sort: string | 434 | sort: string |
435 | }) { | 435 | }) { |
436 | const attributesInclude = [] | 436 | const attributesInclude = [] |
437 | const escapedSearch = VideoModel.sequelize.escape(options.search) | 437 | const escapedSearch = VideoChannelModel.sequelize.escape(options.search) |
438 | const escapedLikeSearch = VideoModel.sequelize.escape('%' + options.search + '%') | 438 | const escapedLikeSearch = VideoChannelModel.sequelize.escape('%' + options.search + '%') |
439 | attributesInclude.push(createSimilarityAttribute('VideoChannelModel.name', options.search)) | 439 | attributesInclude.push(createSimilarityAttribute('VideoChannelModel.name', options.search)) |
440 | 440 | ||
441 | const query = { | 441 | const query = { |
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index 1a05f8d42..7aa6b6c6e 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { join } from 'path' | 1 | import { join } from 'path' |
2 | import { FindOptions, literal, Op, ScopeOptions, Transaction, WhereOptions } from 'sequelize' | 2 | import { FindOptions, literal, Op, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' |
3 | import { | 3 | import { |
4 | AllowNull, | 4 | AllowNull, |
5 | BelongsTo, | 5 | BelongsTo, |
@@ -53,7 +53,15 @@ import { | |||
53 | } from '../../types/models/video/video-playlist' | 53 | } from '../../types/models/video/video-playlist' |
54 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' | 54 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' |
55 | import { ActorModel } from '../actor/actor' | 55 | import { ActorModel } from '../actor/actor' |
56 | import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getPlaylistSort, isOutdated, throwIfNotValid } from '../utils' | 56 | import { |
57 | buildServerIdsFollowedBy, | ||
58 | buildTrigramSearchIndex, | ||
59 | buildWhereIdOrUUID, | ||
60 | createSimilarityAttribute, | ||
61 | getPlaylistSort, | ||
62 | isOutdated, | ||
63 | throwIfNotValid | ||
64 | } from '../utils' | ||
57 | import { ThumbnailModel } from './thumbnail' | 65 | import { ThumbnailModel } from './thumbnail' |
58 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' | 66 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' |
59 | import { VideoPlaylistElementModel } from './video-playlist-element' | 67 | import { VideoPlaylistElementModel } from './video-playlist-element' |
@@ -74,6 +82,11 @@ type AvailableForListOptions = { | |||
74 | videoChannelId?: number | 82 | videoChannelId?: number |
75 | listMyPlaylists?: boolean | 83 | listMyPlaylists?: boolean |
76 | search?: string | 84 | search?: string |
85 | withVideos?: boolean | ||
86 | } | ||
87 | |||
88 | function getVideoLengthSelect () { | ||
89 | return 'SELECT COUNT("id") FROM "videoPlaylistElement" WHERE "videoPlaylistId" = "VideoPlaylistModel"."id"' | ||
77 | } | 90 | } |
78 | 91 | ||
79 | @Scopes(() => ({ | 92 | @Scopes(() => ({ |
@@ -89,7 +102,7 @@ type AvailableForListOptions = { | |||
89 | attributes: { | 102 | attributes: { |
90 | include: [ | 103 | include: [ |
91 | [ | 104 | [ |
92 | literal('(SELECT COUNT("id") FROM "videoPlaylistElement" WHERE "videoPlaylistId" = "VideoPlaylistModel"."id")'), | 105 | literal(`(${getVideoLengthSelect()})`), |
93 | 'videosLength' | 106 | 'videosLength' |
94 | ] | 107 | ] |
95 | ] | 108 | ] |
@@ -178,11 +191,28 @@ type AvailableForListOptions = { | |||
178 | }) | 191 | }) |
179 | } | 192 | } |
180 | 193 | ||
194 | if (options.withVideos === true) { | ||
195 | whereAnd.push( | ||
196 | literal(`(${getVideoLengthSelect()}) != 0`) | ||
197 | ) | ||
198 | } | ||
199 | |||
200 | const attributesInclude = [] | ||
201 | |||
181 | if (options.search) { | 202 | if (options.search) { |
203 | const escapedSearch = VideoPlaylistModel.sequelize.escape(options.search) | ||
204 | const escapedLikeSearch = VideoPlaylistModel.sequelize.escape('%' + options.search + '%') | ||
205 | attributesInclude.push(createSimilarityAttribute('VideoPlaylistModel.name', options.search)) | ||
206 | |||
182 | whereAnd.push({ | 207 | whereAnd.push({ |
183 | name: { | 208 | [Op.or]: [ |
184 | [Op.iLike]: '%' + options.search + '%' | 209 | Sequelize.literal( |
185 | } | 210 | 'lower(immutable_unaccent("VideoPlaylistModel"."name")) % lower(immutable_unaccent(' + escapedSearch + '))' |
211 | ), | ||
212 | Sequelize.literal( | ||
213 | 'lower(immutable_unaccent("VideoPlaylistModel"."name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))' | ||
214 | ) | ||
215 | ] | ||
186 | }) | 216 | }) |
187 | } | 217 | } |
188 | 218 | ||
@@ -191,6 +221,9 @@ type AvailableForListOptions = { | |||
191 | } | 221 | } |
192 | 222 | ||
193 | return { | 223 | return { |
224 | attributes: { | ||
225 | include: attributesInclude | ||
226 | }, | ||
194 | where, | 227 | where, |
195 | include: [ | 228 | include: [ |
196 | { | 229 | { |
@@ -211,6 +244,8 @@ type AvailableForListOptions = { | |||
211 | @Table({ | 244 | @Table({ |
212 | tableName: 'videoPlaylist', | 245 | tableName: 'videoPlaylist', |
213 | indexes: [ | 246 | indexes: [ |
247 | buildTrigramSearchIndex('video_playlist_name_trigram', 'name'), | ||
248 | |||
214 | { | 249 | { |
215 | fields: [ 'ownerAccountId' ] | 250 | fields: [ 'ownerAccountId' ] |
216 | }, | 251 | }, |
@@ -314,6 +349,7 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
314 | videoChannelId?: number | 349 | videoChannelId?: number |
315 | listMyPlaylists?: boolean | 350 | listMyPlaylists?: boolean |
316 | search?: string | 351 | search?: string |
352 | withVideos?: boolean // false by default | ||
317 | }) { | 353 | }) { |
318 | const query = { | 354 | const query = { |
319 | offset: options.start, | 355 | offset: options.start, |
@@ -331,7 +367,8 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
331 | accountId: options.accountId, | 367 | accountId: options.accountId, |
332 | videoChannelId: options.videoChannelId, | 368 | videoChannelId: options.videoChannelId, |
333 | listMyPlaylists: options.listMyPlaylists, | 369 | listMyPlaylists: options.listMyPlaylists, |
334 | search: options.search | 370 | search: options.search, |
371 | withVideos: options.withVideos || false | ||
335 | } as AvailableForListOptions | 372 | } as AvailableForListOptions |
336 | ] | 373 | ] |
337 | }, | 374 | }, |
@@ -347,6 +384,21 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
347 | }) | 384 | }) |
348 | } | 385 | } |
349 | 386 | ||
387 | static searchForApi (options: { | ||
388 | followerActorId: number | ||
389 | start: number | ||
390 | count: number | ||
391 | sort: string | ||
392 | search?: string | ||
393 | }) { | ||
394 | return VideoPlaylistModel.listForApi({ | ||
395 | ...options, | ||
396 | type: VideoPlaylistType.REGULAR, | ||
397 | listMyPlaylists: false, | ||
398 | withVideos: true | ||
399 | }) | ||
400 | } | ||
401 | |||
350 | static listPublicUrlsOfForAP (options: { account?: MAccountId, channel?: MChannelId }, start: number, count: number) { | 402 | static listPublicUrlsOfForAP (options: { account?: MAccountId, channel?: MChannelId }, start: number, count: number) { |
351 | const where = { | 403 | const where = { |
352 | privacy: VideoPlaylistPrivacy.PUBLIC | 404 | privacy: VideoPlaylistPrivacy.PUBLIC |
@@ -445,6 +497,18 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
445 | return VideoPlaylistModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_THUMBNAIL ]).findOne(query) | 497 | return VideoPlaylistModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_THUMBNAIL ]).findOne(query) |
446 | } | 498 | } |
447 | 499 | ||
500 | static loadByUrlWithAccountAndChannelSummary (url: string): Promise<MVideoPlaylistFullSummary> { | ||
501 | const query = { | ||
502 | where: { | ||
503 | url | ||
504 | } | ||
505 | } | ||
506 | |||
507 | return VideoPlaylistModel | ||
508 | .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY, ScopeNames.WITH_VIDEOS_LENGTH, ScopeNames.WITH_THUMBNAIL ]) | ||
509 | .findOne(query) | ||
510 | } | ||
511 | |||
448 | static getPrivacyLabel (privacy: VideoPlaylistPrivacy) { | 512 | static getPrivacyLabel (privacy: VideoPlaylistPrivacy) { |
449 | return VIDEO_PLAYLIST_PRIVACIES[privacy] || 'Unknown' | 513 | return VIDEO_PLAYLIST_PRIVACIES[privacy] || 'Unknown' |
450 | } | 514 | } |
@@ -535,6 +599,10 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
535 | return setAsUpdated('videoPlaylist', this.id) | 599 | return setAsUpdated('videoPlaylist', this.id) |
536 | } | 600 | } |
537 | 601 | ||
602 | setVideosLength (videosLength: number) { | ||
603 | this.set('videosLength' as any, videosLength, { raw: true }) | ||
604 | } | ||
605 | |||
538 | isOwned () { | 606 | isOwned () { |
539 | return this.OwnerAccount.isOwned() | 607 | return this.OwnerAccount.isOwned() |
540 | } | 608 | } |
@@ -551,6 +619,8 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
551 | uuid: this.uuid, | 619 | uuid: this.uuid, |
552 | isLocal: this.isOwned(), | 620 | isLocal: this.isOwned(), |
553 | 621 | ||
622 | url: this.url, | ||
623 | |||
554 | displayName: this.name, | 624 | displayName: this.name, |
555 | description: this.description, | 625 | description: this.description, |
556 | privacy: { | 626 | privacy: { |
diff --git a/server/tests/api/check-params/search.ts b/server/tests/api/check-params/search.ts index 8378c3a89..20ad46cff 100644 --- a/server/tests/api/check-params/search.ts +++ b/server/tests/api/check-params/search.ts | |||
@@ -140,6 +140,30 @@ describe('Test videos API validator', function () { | |||
140 | }) | 140 | }) |
141 | }) | 141 | }) |
142 | 142 | ||
143 | describe('When searching video playlists', function () { | ||
144 | const path = '/api/v1/search/video-playlists/' | ||
145 | |||
146 | const query = { | ||
147 | search: 'coucou' | ||
148 | } | ||
149 | |||
150 | it('Should fail with a bad start pagination', async function () { | ||
151 | await checkBadStartPagination(server.url, path, null, query) | ||
152 | }) | ||
153 | |||
154 | it('Should fail with a bad count pagination', async function () { | ||
155 | await checkBadCountPagination(server.url, path, null, query) | ||
156 | }) | ||
157 | |||
158 | it('Should fail with an incorrect sort', async function () { | ||
159 | await checkBadSortPagination(server.url, path, null, query) | ||
160 | }) | ||
161 | |||
162 | it('Should success with the correct parameters', async function () { | ||
163 | await makeGetRequest({ url: server.url, path, query, statusCodeExpected: HttpStatusCode.OK_200 }) | ||
164 | }) | ||
165 | }) | ||
166 | |||
143 | describe('When searching video channels', function () { | 167 | describe('When searching video channels', function () { |
144 | const path = '/api/v1/search/video-channels/' | 168 | const path = '/api/v1/search/video-channels/' |
145 | 169 | ||
@@ -171,6 +195,7 @@ describe('Test videos API validator', function () { | |||
171 | 195 | ||
172 | const query = { search: 'coucou' } | 196 | const query = { search: 'coucou' } |
173 | const paths = [ | 197 | const paths = [ |
198 | '/api/v1/search/video-playlists/', | ||
174 | '/api/v1/search/video-channels/', | 199 | '/api/v1/search/video-channels/', |
175 | '/api/v1/search/videos/' | 200 | '/api/v1/search/videos/' |
176 | ] | 201 | ] |
diff --git a/server/tests/api/search/index.ts b/server/tests/api/search/index.ts index 232c1f2a4..a976d210d 100644 --- a/server/tests/api/search/index.ts +++ b/server/tests/api/search/index.ts | |||
@@ -1,5 +1,7 @@ | |||
1 | import './search-activitypub-video-playlists' | ||
1 | import './search-activitypub-video-channels' | 2 | import './search-activitypub-video-channels' |
2 | import './search-activitypub-videos' | 3 | import './search-activitypub-videos' |
4 | import './search-channels' | ||
3 | import './search-index' | 5 | import './search-index' |
6 | import './search-playlists' | ||
4 | import './search-videos' | 7 | import './search-videos' |
5 | import './search-channels' | ||
diff --git a/server/tests/api/search/search-activitypub-video-channels.ts b/server/tests/api/search/search-activitypub-video-channels.ts index d7e3ed5be..e83eb7171 100644 --- a/server/tests/api/search/search-activitypub-video-channels.ts +++ b/server/tests/api/search/search-activitypub-video-channels.ts | |||
@@ -106,9 +106,25 @@ describe('Test ActivityPub video channels search', function () { | |||
106 | } | 106 | } |
107 | }) | 107 | }) |
108 | 108 | ||
109 | it('Should search a local video channel with an alternative URL', async function () { | ||
110 | const search = 'http://localhost:' + servers[0].port + '/c/channel1_server1' | ||
111 | |||
112 | for (const token of [ undefined, servers[0].accessToken ]) { | ||
113 | const res = await searchVideoChannel(servers[0].url, search, token) | ||
114 | |||
115 | expect(res.body.total).to.equal(1) | ||
116 | expect(res.body.data).to.be.an('array') | ||
117 | expect(res.body.data).to.have.lengthOf(1) | ||
118 | expect(res.body.data[0].name).to.equal('channel1_server1') | ||
119 | expect(res.body.data[0].displayName).to.equal('Channel 1 server 1') | ||
120 | } | ||
121 | }) | ||
122 | |||
109 | it('Should search a remote video channel with URL or handle', async function () { | 123 | it('Should search a remote video channel with URL or handle', async function () { |
110 | const searches = [ | 124 | const searches = [ |
111 | 'http://localhost:' + servers[1].port + '/video-channels/channel1_server2', | 125 | 'http://localhost:' + servers[1].port + '/video-channels/channel1_server2', |
126 | 'http://localhost:' + servers[1].port + '/c/channel1_server2', | ||
127 | 'http://localhost:' + servers[1].port + '/c/channel1_server2/videos', | ||
112 | 'channel1_server2@localhost:' + servers[1].port | 128 | 'channel1_server2@localhost:' + servers[1].port |
113 | ] | 129 | ] |
114 | 130 | ||
diff --git a/server/tests/api/search/search-activitypub-video-playlists.ts b/server/tests/api/search/search-activitypub-video-playlists.ts new file mode 100644 index 000000000..4c08e9548 --- /dev/null +++ b/server/tests/api/search/search-activitypub-video-playlists.ts | |||
@@ -0,0 +1,212 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import * as chai from 'chai' | ||
5 | import { | ||
6 | addVideoInPlaylist, | ||
7 | cleanupTests, | ||
8 | createVideoPlaylist, | ||
9 | deleteVideoPlaylist, | ||
10 | flushAndRunMultipleServers, | ||
11 | getVideoPlaylistsList, | ||
12 | searchVideoPlaylists, | ||
13 | ServerInfo, | ||
14 | setAccessTokensToServers, | ||
15 | setDefaultVideoChannel, | ||
16 | uploadVideoAndGetId, | ||
17 | wait | ||
18 | } from '../../../../shared/extra-utils' | ||
19 | import { waitJobs } from '../../../../shared/extra-utils/server/jobs' | ||
20 | import { VideoPlaylist, VideoPlaylistPrivacy } from '../../../../shared/models/videos' | ||
21 | |||
22 | const expect = chai.expect | ||
23 | |||
24 | describe('Test ActivityPub playlists search', function () { | ||
25 | let servers: ServerInfo[] | ||
26 | let playlistServer1UUID: string | ||
27 | let playlistServer2UUID: string | ||
28 | let video2Server2: string | ||
29 | |||
30 | before(async function () { | ||
31 | this.timeout(120000) | ||
32 | |||
33 | servers = await flushAndRunMultipleServers(2) | ||
34 | |||
35 | await setAccessTokensToServers(servers) | ||
36 | await setDefaultVideoChannel(servers) | ||
37 | |||
38 | { | ||
39 | const video1 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 1' })).uuid | ||
40 | const video2 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 2' })).uuid | ||
41 | |||
42 | const attributes = { | ||
43 | displayName: 'playlist 1 on server 1', | ||
44 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
45 | videoChannelId: servers[0].videoChannel.id | ||
46 | } | ||
47 | const res = await createVideoPlaylist({ url: servers[0].url, token: servers[0].accessToken, playlistAttrs: attributes }) | ||
48 | playlistServer1UUID = res.body.videoPlaylist.uuid | ||
49 | |||
50 | for (const videoId of [ video1, video2 ]) { | ||
51 | await addVideoInPlaylist({ | ||
52 | url: servers[0].url, | ||
53 | token: servers[0].accessToken, | ||
54 | playlistId: playlistServer1UUID, | ||
55 | elementAttrs: { videoId } | ||
56 | }) | ||
57 | } | ||
58 | } | ||
59 | |||
60 | { | ||
61 | const videoId = (await uploadVideoAndGetId({ server: servers[1], videoName: 'video 1' })).uuid | ||
62 | video2Server2 = (await uploadVideoAndGetId({ server: servers[1], videoName: 'video 2' })).uuid | ||
63 | |||
64 | const attributes = { | ||
65 | displayName: 'playlist 1 on server 2', | ||
66 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
67 | videoChannelId: servers[1].videoChannel.id | ||
68 | } | ||
69 | const res = await createVideoPlaylist({ url: servers[1].url, token: servers[1].accessToken, playlistAttrs: attributes }) | ||
70 | playlistServer2UUID = res.body.videoPlaylist.uuid | ||
71 | |||
72 | await addVideoInPlaylist({ | ||
73 | url: servers[1].url, | ||
74 | token: servers[1].accessToken, | ||
75 | playlistId: playlistServer2UUID, | ||
76 | elementAttrs: { videoId } | ||
77 | }) | ||
78 | } | ||
79 | |||
80 | await waitJobs(servers) | ||
81 | }) | ||
82 | |||
83 | it('Should not find a remote playlist', async function () { | ||
84 | { | ||
85 | const search = 'http://localhost:' + servers[1].port + '/video-playlists/43' | ||
86 | const res = await searchVideoPlaylists(servers[0].url, search, servers[0].accessToken) | ||
87 | |||
88 | expect(res.body.total).to.equal(0) | ||
89 | expect(res.body.data).to.be.an('array') | ||
90 | expect(res.body.data).to.have.lengthOf(0) | ||
91 | } | ||
92 | |||
93 | { | ||
94 | // Without token | ||
95 | const search = 'http://localhost:' + servers[1].port + '/video-playlists/' + playlistServer2UUID | ||
96 | const res = await searchVideoPlaylists(servers[0].url, search) | ||
97 | |||
98 | expect(res.body.total).to.equal(0) | ||
99 | expect(res.body.data).to.be.an('array') | ||
100 | expect(res.body.data).to.have.lengthOf(0) | ||
101 | } | ||
102 | }) | ||
103 | |||
104 | it('Should search a local playlist', async function () { | ||
105 | const search = 'http://localhost:' + servers[0].port + '/video-playlists/' + playlistServer1UUID | ||
106 | const res = await searchVideoPlaylists(servers[0].url, search) | ||
107 | |||
108 | expect(res.body.total).to.equal(1) | ||
109 | expect(res.body.data).to.be.an('array') | ||
110 | expect(res.body.data).to.have.lengthOf(1) | ||
111 | expect(res.body.data[0].displayName).to.equal('playlist 1 on server 1') | ||
112 | expect(res.body.data[0].videosLength).to.equal(2) | ||
113 | }) | ||
114 | |||
115 | it('Should search a local playlist with an alternative URL', async function () { | ||
116 | const searches = [ | ||
117 | 'http://localhost:' + servers[0].port + '/videos/watch/playlist/' + playlistServer1UUID, | ||
118 | 'http://localhost:' + servers[0].port + '/w/p/' + playlistServer1UUID | ||
119 | ] | ||
120 | |||
121 | for (const search of searches) { | ||
122 | for (const token of [ undefined, servers[0].accessToken ]) { | ||
123 | const res = await searchVideoPlaylists(servers[0].url, search, token) | ||
124 | |||
125 | expect(res.body.total).to.equal(1) | ||
126 | expect(res.body.data).to.be.an('array') | ||
127 | expect(res.body.data).to.have.lengthOf(1) | ||
128 | expect(res.body.data[0].displayName).to.equal('playlist 1 on server 1') | ||
129 | expect(res.body.data[0].videosLength).to.equal(2) | ||
130 | } | ||
131 | } | ||
132 | }) | ||
133 | |||
134 | it('Should search a remote playlist', async function () { | ||
135 | const searches = [ | ||
136 | 'http://localhost:' + servers[1].port + '/video-playlists/' + playlistServer2UUID, | ||
137 | 'http://localhost:' + servers[1].port + '/videos/watch/playlist/' + playlistServer2UUID, | ||
138 | 'http://localhost:' + servers[1].port + '/w/p/' + playlistServer2UUID | ||
139 | ] | ||
140 | |||
141 | for (const search of searches) { | ||
142 | const res = await searchVideoPlaylists(servers[0].url, search, servers[0].accessToken) | ||
143 | |||
144 | expect(res.body.total).to.equal(1) | ||
145 | expect(res.body.data).to.be.an('array') | ||
146 | expect(res.body.data).to.have.lengthOf(1) | ||
147 | expect(res.body.data[0].displayName).to.equal('playlist 1 on server 2') | ||
148 | expect(res.body.data[0].videosLength).to.equal(1) | ||
149 | } | ||
150 | }) | ||
151 | |||
152 | it('Should not list this remote playlist', async function () { | ||
153 | const res = await getVideoPlaylistsList(servers[0].url, 0, 10) | ||
154 | expect(res.body.total).to.equal(1) | ||
155 | expect(res.body.data).to.have.lengthOf(1) | ||
156 | expect(res.body.data[0].displayName).to.equal('playlist 1 on server 1') | ||
157 | }) | ||
158 | |||
159 | it('Should update the playlist of server 2, and refresh it on server 1', async function () { | ||
160 | this.timeout(60000) | ||
161 | |||
162 | await addVideoInPlaylist({ | ||
163 | url: servers[1].url, | ||
164 | token: servers[1].accessToken, | ||
165 | playlistId: playlistServer2UUID, | ||
166 | elementAttrs: { videoId: video2Server2 } | ||
167 | }) | ||
168 | |||
169 | await waitJobs(servers) | ||
170 | // Expire playlist | ||
171 | await wait(10000) | ||
172 | |||
173 | // Will run refresh async | ||
174 | const search = 'http://localhost:' + servers[1].port + '/video-playlists/' + playlistServer2UUID | ||
175 | await searchVideoPlaylists(servers[0].url, search, servers[0].accessToken) | ||
176 | |||
177 | // Wait refresh | ||
178 | await wait(5000) | ||
179 | |||
180 | const res = await searchVideoPlaylists(servers[0].url, search, servers[0].accessToken) | ||
181 | expect(res.body.total).to.equal(1) | ||
182 | expect(res.body.data).to.have.lengthOf(1) | ||
183 | |||
184 | const playlist: VideoPlaylist = res.body.data[0] | ||
185 | expect(playlist.videosLength).to.equal(2) | ||
186 | }) | ||
187 | |||
188 | it('Should delete playlist of server 2, and delete it on server 1', async function () { | ||
189 | this.timeout(60000) | ||
190 | |||
191 | await deleteVideoPlaylist(servers[1].url, servers[1].accessToken, playlistServer2UUID) | ||
192 | |||
193 | await waitJobs(servers) | ||
194 | // Expiration | ||
195 | await wait(10000) | ||
196 | |||
197 | // Will run refresh async | ||
198 | const search = 'http://localhost:' + servers[1].port + '/video-playlists/' + playlistServer2UUID | ||
199 | await searchVideoPlaylists(servers[0].url, search, servers[0].accessToken) | ||
200 | |||
201 | // Wait refresh | ||
202 | await wait(5000) | ||
203 | |||
204 | const res = await searchVideoPlaylists(servers[0].url, search, servers[0].accessToken) | ||
205 | expect(res.body.total).to.equal(0) | ||
206 | expect(res.body.data).to.have.lengthOf(0) | ||
207 | }) | ||
208 | |||
209 | after(async function () { | ||
210 | await cleanupTests(servers) | ||
211 | }) | ||
212 | }) | ||
diff --git a/server/tests/api/search/search-activitypub-videos.ts b/server/tests/api/search/search-activitypub-videos.ts index c62dfca0d..e9b4978da 100644 --- a/server/tests/api/search/search-activitypub-videos.ts +++ b/server/tests/api/search/search-activitypub-videos.ts | |||
@@ -77,14 +77,33 @@ describe('Test ActivityPub videos search', function () { | |||
77 | expect(res.body.data[0].name).to.equal('video 1 on server 1') | 77 | expect(res.body.data[0].name).to.equal('video 1 on server 1') |
78 | }) | 78 | }) |
79 | 79 | ||
80 | it('Should search a local video with an alternative URL', async function () { | ||
81 | const search = 'http://localhost:' + servers[0].port + '/w/' + videoServer1UUID | ||
82 | const res1 = await searchVideo(servers[0].url, search) | ||
83 | const res2 = await searchVideoWithToken(servers[0].url, search, servers[0].accessToken) | ||
84 | |||
85 | for (const res of [ res1, res2 ]) { | ||
86 | expect(res.body.total).to.equal(1) | ||
87 | expect(res.body.data).to.be.an('array') | ||
88 | expect(res.body.data).to.have.lengthOf(1) | ||
89 | expect(res.body.data[0].name).to.equal('video 1 on server 1') | ||
90 | } | ||
91 | }) | ||
92 | |||
80 | it('Should search a remote video', async function () { | 93 | it('Should search a remote video', async function () { |
81 | const search = 'http://localhost:' + servers[1].port + '/videos/watch/' + videoServer2UUID | 94 | const searches = [ |
82 | const res = await searchVideoWithToken(servers[0].url, search, servers[0].accessToken) | 95 | 'http://localhost:' + servers[1].port + '/w/' + videoServer2UUID, |
96 | 'http://localhost:' + servers[1].port + '/videos/watch/' + videoServer2UUID | ||
97 | ] | ||
83 | 98 | ||
84 | expect(res.body.total).to.equal(1) | 99 | for (const search of searches) { |
85 | expect(res.body.data).to.be.an('array') | 100 | const res = await searchVideoWithToken(servers[0].url, search, servers[0].accessToken) |
86 | expect(res.body.data).to.have.lengthOf(1) | 101 | |
87 | expect(res.body.data[0].name).to.equal('video 1 on server 2') | 102 | expect(res.body.total).to.equal(1) |
103 | expect(res.body.data).to.be.an('array') | ||
104 | expect(res.body.data).to.have.lengthOf(1) | ||
105 | expect(res.body.data[0].name).to.equal('video 1 on server 2') | ||
106 | } | ||
88 | }) | 107 | }) |
89 | 108 | ||
90 | it('Should not list this remote video', async function () { | 109 | it('Should not list this remote video', async function () { |
@@ -95,7 +114,7 @@ describe('Test ActivityPub videos search', function () { | |||
95 | }) | 114 | }) |
96 | 115 | ||
97 | it('Should update video of server 2, and refresh it on server 1', async function () { | 116 | it('Should update video of server 2, and refresh it on server 1', async function () { |
98 | this.timeout(60000) | 117 | this.timeout(120000) |
99 | 118 | ||
100 | const channelAttributes = { | 119 | const channelAttributes = { |
101 | name: 'super_channel', | 120 | name: 'super_channel', |
@@ -134,7 +153,7 @@ describe('Test ActivityPub videos search', function () { | |||
134 | }) | 153 | }) |
135 | 154 | ||
136 | it('Should delete video of server 2, and delete it on server 1', async function () { | 155 | it('Should delete video of server 2, and delete it on server 1', async function () { |
137 | this.timeout(60000) | 156 | this.timeout(120000) |
138 | 157 | ||
139 | await removeVideo(servers[1].url, servers[1].accessToken, videoServer2UUID) | 158 | await removeVideo(servers[1].url, servers[1].accessToken, videoServer2UUID) |
140 | 159 | ||
diff --git a/server/tests/api/search/search-index.ts b/server/tests/api/search/search-index.ts index 849a8a893..00f79232a 100644 --- a/server/tests/api/search/search-index.ts +++ b/server/tests/api/search/search-index.ts | |||
@@ -2,19 +2,21 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { advancedVideoChannelSearch, searchVideoChannel } from '@shared/extra-utils/search/video-channels' | ||
6 | import { Video, VideoChannel, VideoPlaylist, VideoPlaylistPrivacy, VideoPlaylistType, VideosSearchQuery } from '@shared/models' | ||
5 | import { | 7 | import { |
8 | advancedVideoPlaylistSearch, | ||
9 | advancedVideosSearch, | ||
6 | cleanupTests, | 10 | cleanupTests, |
7 | flushAndRunServer, | 11 | flushAndRunServer, |
12 | immutableAssign, | ||
8 | searchVideo, | 13 | searchVideo, |
14 | searchVideoPlaylists, | ||
9 | ServerInfo, | 15 | ServerInfo, |
10 | setAccessTokensToServers, | 16 | setAccessTokensToServers, |
11 | updateCustomSubConfig, | 17 | updateCustomSubConfig, |
12 | uploadVideo, | 18 | uploadVideo |
13 | advancedVideosSearch, | ||
14 | immutableAssign | ||
15 | } from '../../../../shared/extra-utils' | 19 | } from '../../../../shared/extra-utils' |
16 | import { searchVideoChannel, advancedVideoChannelSearch } from '@shared/extra-utils/search/video-channels' | ||
17 | import { VideosSearchQuery, Video, VideoChannel } from '@shared/models' | ||
18 | 20 | ||
19 | const expect = chai.expect | 21 | const expect = chai.expect |
20 | 22 | ||
@@ -277,6 +279,56 @@ describe('Test videos search', function () { | |||
277 | }) | 279 | }) |
278 | }) | 280 | }) |
279 | 281 | ||
282 | describe('Playlists search', async function () { | ||
283 | |||
284 | it('Should make a simple search and not have results', async function () { | ||
285 | const res = await searchVideoPlaylists(server.url, 'a'.repeat(500)) | ||
286 | |||
287 | expect(res.body.total).to.equal(0) | ||
288 | expect(res.body.data).to.have.lengthOf(0) | ||
289 | }) | ||
290 | |||
291 | it('Should make a search and have results', async function () { | ||
292 | const res = await advancedVideoPlaylistSearch(server.url, { search: 'E2E playlist', sort: '-match' }) | ||
293 | |||
294 | expect(res.body.total).to.be.greaterThan(0) | ||
295 | expect(res.body.data).to.have.length.greaterThan(0) | ||
296 | |||
297 | const videoPlaylist: VideoPlaylist = res.body.data[0] | ||
298 | |||
299 | expect(videoPlaylist.url).to.equal('https://peertube2.cpy.re/videos/watch/playlist/73804a40-da9a-40c2-b1eb-2c6d9eec8f0a') | ||
300 | expect(videoPlaylist.thumbnailUrl).to.exist | ||
301 | expect(videoPlaylist.embedUrl).to.equal('https://peertube2.cpy.re/video-playlists/embed/73804a40-da9a-40c2-b1eb-2c6d9eec8f0a') | ||
302 | |||
303 | expect(videoPlaylist.type.id).to.equal(VideoPlaylistType.REGULAR) | ||
304 | expect(videoPlaylist.privacy.id).to.equal(VideoPlaylistPrivacy.PUBLIC) | ||
305 | expect(videoPlaylist.videosLength).to.exist | ||
306 | |||
307 | expect(videoPlaylist.createdAt).to.exist | ||
308 | expect(videoPlaylist.updatedAt).to.exist | ||
309 | |||
310 | expect(videoPlaylist.uuid).to.equal('73804a40-da9a-40c2-b1eb-2c6d9eec8f0a') | ||
311 | expect(videoPlaylist.displayName).to.exist | ||
312 | |||
313 | expect(videoPlaylist.ownerAccount.url).to.equal('https://peertube2.cpy.re/accounts/chocobozzz') | ||
314 | expect(videoPlaylist.ownerAccount.name).to.equal('chocobozzz') | ||
315 | expect(videoPlaylist.ownerAccount.host).to.equal('peertube2.cpy.re') | ||
316 | expect(videoPlaylist.ownerAccount.avatar).to.exist | ||
317 | |||
318 | expect(videoPlaylist.videoChannel.url).to.equal('https://peertube2.cpy.re/video-channels/chocobozzz_channel') | ||
319 | expect(videoPlaylist.videoChannel.name).to.equal('chocobozzz_channel') | ||
320 | expect(videoPlaylist.videoChannel.host).to.equal('peertube2.cpy.re') | ||
321 | expect(videoPlaylist.videoChannel.avatar).to.exist | ||
322 | }) | ||
323 | |||
324 | it('Should have a correct pagination', async function () { | ||
325 | const res = await advancedVideoChannelSearch(server.url, { search: 'root', start: 0, count: 2 }) | ||
326 | |||
327 | expect(res.body.total).to.be.greaterThan(2) | ||
328 | expect(res.body.data).to.have.lengthOf(2) | ||
329 | }) | ||
330 | }) | ||
331 | |||
280 | after(async function () { | 332 | after(async function () { |
281 | await cleanupTests([ server ]) | 333 | await cleanupTests([ server ]) |
282 | }) | 334 | }) |
diff --git a/server/tests/api/search/search-playlists.ts b/server/tests/api/search/search-playlists.ts new file mode 100644 index 000000000..ab17d55e9 --- /dev/null +++ b/server/tests/api/search/search-playlists.ts | |||
@@ -0,0 +1,128 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import * as chai from 'chai' | ||
5 | import { VideoPlaylist, VideoPlaylistPrivacy } from '@shared/models' | ||
6 | import { | ||
7 | addVideoInPlaylist, | ||
8 | advancedVideoPlaylistSearch, | ||
9 | cleanupTests, | ||
10 | createVideoPlaylist, | ||
11 | flushAndRunServer, | ||
12 | searchVideoPlaylists, | ||
13 | ServerInfo, | ||
14 | setAccessTokensToServers, | ||
15 | setDefaultVideoChannel, | ||
16 | uploadVideoAndGetId | ||
17 | } from '../../../../shared/extra-utils' | ||
18 | |||
19 | const expect = chai.expect | ||
20 | |||
21 | describe('Test playlists search', function () { | ||
22 | let server: ServerInfo = null | ||
23 | |||
24 | before(async function () { | ||
25 | this.timeout(30000) | ||
26 | |||
27 | server = await flushAndRunServer(1) | ||
28 | |||
29 | await setAccessTokensToServers([ server ]) | ||
30 | await setDefaultVideoChannel([ server ]) | ||
31 | |||
32 | const videoId = (await uploadVideoAndGetId({ server: server, videoName: 'video' })).uuid | ||
33 | |||
34 | { | ||
35 | const attributes = { | ||
36 | displayName: 'Dr. Kenzo Tenma hospital videos', | ||
37 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
38 | videoChannelId: server.videoChannel.id | ||
39 | } | ||
40 | const res = await createVideoPlaylist({ url: server.url, token: server.accessToken, playlistAttrs: attributes }) | ||
41 | |||
42 | await addVideoInPlaylist({ | ||
43 | url: server.url, | ||
44 | token: server.accessToken, | ||
45 | playlistId: res.body.videoPlaylist.id, | ||
46 | elementAttrs: { videoId } | ||
47 | }) | ||
48 | } | ||
49 | |||
50 | { | ||
51 | const attributes = { | ||
52 | displayName: 'Johan & Anna Libert musics', | ||
53 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
54 | videoChannelId: server.videoChannel.id | ||
55 | } | ||
56 | const res = await createVideoPlaylist({ url: server.url, token: server.accessToken, playlistAttrs: attributes }) | ||
57 | |||
58 | await addVideoInPlaylist({ | ||
59 | url: server.url, | ||
60 | token: server.accessToken, | ||
61 | playlistId: res.body.videoPlaylist.id, | ||
62 | elementAttrs: { videoId } | ||
63 | }) | ||
64 | } | ||
65 | |||
66 | { | ||
67 | const attributes = { | ||
68 | displayName: 'Inspector Lunge playlist', | ||
69 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
70 | videoChannelId: server.videoChannel.id | ||
71 | } | ||
72 | await createVideoPlaylist({ url: server.url, token: server.accessToken, playlistAttrs: attributes }) | ||
73 | } | ||
74 | }) | ||
75 | |||
76 | it('Should make a simple search and not have results', async function () { | ||
77 | const res = await searchVideoPlaylists(server.url, 'abc') | ||
78 | |||
79 | expect(res.body.total).to.equal(0) | ||
80 | expect(res.body.data).to.have.lengthOf(0) | ||
81 | }) | ||
82 | |||
83 | it('Should make a search and have results', async function () { | ||
84 | { | ||
85 | const search = { | ||
86 | search: 'tenma', | ||
87 | start: 0, | ||
88 | count: 1 | ||
89 | } | ||
90 | const res = await advancedVideoPlaylistSearch(server.url, search) | ||
91 | expect(res.body.total).to.equal(1) | ||
92 | expect(res.body.data).to.have.lengthOf(1) | ||
93 | |||
94 | const playlist: VideoPlaylist = res.body.data[0] | ||
95 | expect(playlist.displayName).to.equal('Dr. Kenzo Tenma hospital videos') | ||
96 | expect(playlist.url).to.equal(server.url + '/video-playlists/' + playlist.uuid) | ||
97 | } | ||
98 | |||
99 | { | ||
100 | const search = { | ||
101 | search: 'Anna Livert', | ||
102 | start: 0, | ||
103 | count: 1 | ||
104 | } | ||
105 | const res = await advancedVideoPlaylistSearch(server.url, search) | ||
106 | expect(res.body.total).to.equal(1) | ||
107 | expect(res.body.data).to.have.lengthOf(1) | ||
108 | |||
109 | const playlist: VideoPlaylist = res.body.data[0] | ||
110 | expect(playlist.displayName).to.equal('Johan & Anna Libert musics') | ||
111 | } | ||
112 | }) | ||
113 | |||
114 | it('Should not display playlists without videos', async function () { | ||
115 | const search = { | ||
116 | search: 'Lunge', | ||
117 | start: 0, | ||
118 | count: 1 | ||
119 | } | ||
120 | const res = await advancedVideoPlaylistSearch(server.url, search) | ||
121 | expect(res.body.total).to.equal(0) | ||
122 | expect(res.body.data).to.have.lengthOf(0) | ||
123 | }) | ||
124 | |||
125 | after(async function () { | ||
126 | await cleanupTests([ server ]) | ||
127 | }) | ||
128 | }) | ||
diff --git a/server/tests/fixtures/peertube-plugin-test-two/languages/fr.json b/server/tests/fixtures/peertube-plugin-test-filter-translations/languages/fr.json index 52d8313df..52d8313df 100644 --- a/server/tests/fixtures/peertube-plugin-test-two/languages/fr.json +++ b/server/tests/fixtures/peertube-plugin-test-filter-translations/languages/fr.json | |||
diff --git a/server/tests/fixtures/peertube-plugin-test-two/languages/it.json b/server/tests/fixtures/peertube-plugin-test-filter-translations/languages/it.json index 9e187d83b..9e187d83b 100644 --- a/server/tests/fixtures/peertube-plugin-test-two/languages/it.json +++ b/server/tests/fixtures/peertube-plugin-test-filter-translations/languages/it.json | |||
diff --git a/server/tests/fixtures/peertube-plugin-test-two/main.js b/server/tests/fixtures/peertube-plugin-test-filter-translations/main.js index 71c11b2ba..71c11b2ba 100644 --- a/server/tests/fixtures/peertube-plugin-test-two/main.js +++ b/server/tests/fixtures/peertube-plugin-test-filter-translations/main.js | |||
diff --git a/server/tests/fixtures/peertube-plugin-test-two/package.json b/server/tests/fixtures/peertube-plugin-test-filter-translations/package.json index 926f2d69b..2adce4743 100644 --- a/server/tests/fixtures/peertube-plugin-test-two/package.json +++ b/server/tests/fixtures/peertube-plugin-test-filter-translations/package.json | |||
@@ -1,7 +1,7 @@ | |||
1 | { | 1 | { |
2 | "name": "peertube-plugin-test-two", | 2 | "name": "peertube-plugin-test-filter-translations", |
3 | "version": "0.0.1", | 3 | "version": "0.0.1", |
4 | "description": "Plugin test 2", | 4 | "description": "Plugin test filter and translations", |
5 | "engine": { | 5 | "engine": { |
6 | "peertube": ">=1.3.0" | 6 | "peertube": ">=1.3.0" |
7 | }, | 7 | }, |
diff --git a/server/tests/fixtures/peertube-plugin-test/main.js b/server/tests/fixtures/peertube-plugin-test/main.js index ee0bc39f3..5e922ad1f 100644 --- a/server/tests/fixtures/peertube-plugin-test/main.js +++ b/server/tests/fixtures/peertube-plugin-test/main.js | |||
@@ -241,6 +241,10 @@ async function register ({ registerHook, registerSetting, settingsManager, stora | |||
241 | 'filter:api.search.video-channels.local.list.result', | 241 | 'filter:api.search.video-channels.local.list.result', |
242 | 'filter:api.search.video-channels.index.list.params', | 242 | 'filter:api.search.video-channels.index.list.params', |
243 | 'filter:api.search.video-channels.index.list.result', | 243 | 'filter:api.search.video-channels.index.list.result', |
244 | 'filter:api.search.video-playlists.local.list.params', | ||
245 | 'filter:api.search.video-playlists.local.list.result', | ||
246 | 'filter:api.search.video-playlists.index.list.params', | ||
247 | 'filter:api.search.video-playlists.index.list.result' | ||
244 | ] | 248 | ] |
245 | 249 | ||
246 | for (const h of searchHooks) { | 250 | for (const h of searchHooks) { |
diff --git a/server/tests/plugins/filter-hooks.ts b/server/tests/plugins/filter-hooks.ts index a947283c2..644b41dea 100644 --- a/server/tests/plugins/filter-hooks.ts +++ b/server/tests/plugins/filter-hooks.ts | |||
@@ -8,6 +8,7 @@ import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-code | |||
8 | import { | 8 | import { |
9 | addVideoCommentReply, | 9 | addVideoCommentReply, |
10 | addVideoCommentThread, | 10 | addVideoCommentThread, |
11 | advancedVideoPlaylistSearch, | ||
11 | advancedVideosSearch, | 12 | advancedVideosSearch, |
12 | createLive, | 13 | createLive, |
13 | createVideoPlaylist, | 14 | createVideoPlaylist, |
@@ -71,7 +72,7 @@ describe('Test plugin filter hooks', function () { | |||
71 | await installPlugin({ | 72 | await installPlugin({ |
72 | url: servers[0].url, | 73 | url: servers[0].url, |
73 | accessToken: servers[0].accessToken, | 74 | accessToken: servers[0].accessToken, |
74 | path: getPluginTestPath('-two') | 75 | path: getPluginTestPath('-filter-translations') |
75 | }) | 76 | }) |
76 | 77 | ||
77 | for (let i = 0; i < 10; i++) { | 78 | for (let i = 0; i < 10; i++) { |
@@ -525,6 +526,27 @@ describe('Test plugin filter hooks', function () { | |||
525 | await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.index.list.params', 1) | 526 | await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.index.list.params', 1) |
526 | await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.index.list.result', 1) | 527 | await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.index.list.result', 1) |
527 | }) | 528 | }) |
529 | |||
530 | it('Should run filter:api.search.video-playlists.local.list.{params,result}', async function () { | ||
531 | await advancedVideoPlaylistSearch(servers[0].url, { | ||
532 | search: 'Sun Jian' | ||
533 | }) | ||
534 | |||
535 | await waitUntilLog(servers[0], 'Run hook filter:api.search.video-playlists.local.list.params', 1) | ||
536 | await waitUntilLog(servers[0], 'Run hook filter:api.search.video-playlists.local.list.result', 1) | ||
537 | }) | ||
538 | |||
539 | it('Should run filter:api.search.video-playlists.index.list.{params,result}', async function () { | ||
540 | await advancedVideoPlaylistSearch(servers[0].url, { | ||
541 | search: 'Sun Jian', | ||
542 | searchTarget: 'search-index' | ||
543 | }) | ||
544 | |||
545 | await waitUntilLog(servers[0], 'Run hook filter:api.search.video-playlists.local.list.params', 1) | ||
546 | await waitUntilLog(servers[0], 'Run hook filter:api.search.video-playlists.local.list.result', 1) | ||
547 | await waitUntilLog(servers[0], 'Run hook filter:api.search.video-playlists.index.list.params', 1) | ||
548 | await waitUntilLog(servers[0], 'Run hook filter:api.search.video-playlists.index.list.result', 1) | ||
549 | }) | ||
528 | }) | 550 | }) |
529 | 551 | ||
530 | after(async function () { | 552 | after(async function () { |
diff --git a/server/tests/plugins/translations.ts b/server/tests/plugins/translations.ts index 8dc2043b8..9fd2ba1c5 100644 --- a/server/tests/plugins/translations.ts +++ b/server/tests/plugins/translations.ts | |||
@@ -31,7 +31,7 @@ describe('Test plugin translations', function () { | |||
31 | await installPlugin({ | 31 | await installPlugin({ |
32 | url: server.url, | 32 | url: server.url, |
33 | accessToken: server.accessToken, | 33 | accessToken: server.accessToken, |
34 | path: getPluginTestPath('-two') | 34 | path: getPluginTestPath('-filter-translations') |
35 | }) | 35 | }) |
36 | }) | 36 | }) |
37 | 37 | ||
@@ -48,7 +48,7 @@ describe('Test plugin translations', function () { | |||
48 | 'peertube-plugin-test': { | 48 | 'peertube-plugin-test': { |
49 | Hi: 'Coucou' | 49 | Hi: 'Coucou' |
50 | }, | 50 | }, |
51 | 'peertube-plugin-test-two': { | 51 | 'peertube-plugin-test-filter-translations': { |
52 | 'Hello world': 'Bonjour le monde' | 52 | 'Hello world': 'Bonjour le monde' |
53 | } | 53 | } |
54 | }) | 54 | }) |
@@ -58,14 +58,14 @@ describe('Test plugin translations', function () { | |||
58 | const res = await getPluginTranslations({ url: server.url, locale: 'it-IT' }) | 58 | const res = await getPluginTranslations({ url: server.url, locale: 'it-IT' }) |
59 | 59 | ||
60 | expect(res.body).to.deep.equal({ | 60 | expect(res.body).to.deep.equal({ |
61 | 'peertube-plugin-test-two': { | 61 | 'peertube-plugin-test-filter-translations': { |
62 | 'Hello world': 'Ciao, mondo!' | 62 | 'Hello world': 'Ciao, mondo!' |
63 | } | 63 | } |
64 | }) | 64 | }) |
65 | }) | 65 | }) |
66 | 66 | ||
67 | it('Should remove the plugin and remove the locales', async function () { | 67 | it('Should remove the plugin and remove the locales', async function () { |
68 | await uninstallPlugin({ url: server.url, accessToken: server.accessToken, npmName: 'peertube-plugin-test-two' }) | 68 | await uninstallPlugin({ url: server.url, accessToken: server.accessToken, npmName: 'peertube-plugin-test-filter-translations' }) |
69 | 69 | ||
70 | { | 70 | { |
71 | const res = await getPluginTranslations({ url: server.url, locale: 'fr-FR' }) | 71 | const res = await getPluginTranslations({ url: server.url, locale: 'fr-FR' }) |
diff --git a/server/types/models/video/video-playlist.ts b/server/types/models/video/video-playlist.ts index 79e2daebf..2f9537cf5 100644 --- a/server/types/models/video/video-playlist.ts +++ b/server/types/models/video/video-playlist.ts | |||
@@ -69,7 +69,7 @@ export type MVideoPlaylistAccountChannelDefault = | |||
69 | // With all associations | 69 | // With all associations |
70 | 70 | ||
71 | export type MVideoPlaylistFull = | 71 | export type MVideoPlaylistFull = |
72 | MVideoPlaylist & | 72 | MVideoPlaylistVideosLength & |
73 | Use<'OwnerAccount', MAccountDefault> & | 73 | Use<'OwnerAccount', MAccountDefault> & |
74 | Use<'VideoChannel', MChannelDefault> & | 74 | Use<'VideoChannel', MChannelDefault> & |
75 | Use<'Thumbnail', MThumbnail> | 75 | Use<'Thumbnail', MThumbnail> |
@@ -84,7 +84,7 @@ export type MVideoPlaylistAccountChannelSummary = | |||
84 | Use<'VideoChannel', MChannelSummary> | 84 | Use<'VideoChannel', MChannelSummary> |
85 | 85 | ||
86 | export type MVideoPlaylistFullSummary = | 86 | export type MVideoPlaylistFullSummary = |
87 | MVideoPlaylist & | 87 | MVideoPlaylistVideosLength & |
88 | Use<'Thumbnail', MThumbnail> & | 88 | Use<'Thumbnail', MThumbnail> & |
89 | Use<'OwnerAccount', MAccountSummary> & | 89 | Use<'OwnerAccount', MAccountSummary> & |
90 | Use<'VideoChannel', MChannelSummary> | 90 | Use<'VideoChannel', MChannelSummary> |