From 37a44fc915eef2140e22ceb96aba6b6eb2509007 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 17 Jun 2021 16:02:38 +0200 Subject: Add ability to search playlists --- server/controllers/api/search/index.ts | 16 +++ .../api/search/search-video-channels.ts | 150 ++++++++++++++++++++ .../api/search/search-video-playlists.ts | 129 +++++++++++++++++ server/controllers/api/search/search-videos.ts | 153 +++++++++++++++++++++ 4 files changed, 448 insertions(+) create mode 100644 server/controllers/api/search/index.ts create mode 100644 server/controllers/api/search/search-video-channels.ts create mode 100644 server/controllers/api/search/search-video-playlists.ts create mode 100644 server/controllers/api/search/search-videos.ts (limited to 'server/controllers/api/search') 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 @@ +import * as express from 'express' +import { searchChannelsRouter } from './search-video-channels' +import { searchPlaylistsRouter } from './search-video-playlists' +import { searchVideosRouter } from './search-videos' + +const searchRouter = express.Router() + +searchRouter.use('/', searchVideosRouter) +searchRouter.use('/', searchChannelsRouter) +searchRouter.use('/', searchPlaylistsRouter) + +// --------------------------------------------------------------------------- + +export { + searchRouter +} 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 @@ +import * as express from 'express' +import { sanitizeUrl } from '@server/helpers/core-utils' +import { doJSONRequest } from '@server/helpers/requests' +import { CONFIG } from '@server/initializers/config' +import { WEBSERVER } from '@server/initializers/constants' +import { Hooks } from '@server/lib/plugins/hooks' +import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search' +import { getServerActor } from '@server/models/application/application' +import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' +import { ResultList, VideoChannel } from '@shared/models' +import { VideoChannelsSearchQuery } from '../../../../shared/models/search' +import { isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils' +import { logger } from '../../../helpers/logger' +import { getFormattedObjects } from '../../../helpers/utils' +import { getOrCreateAPActor, loadActorUrlOrGetFromWebfinger } from '../../../lib/activitypub/actors' +import { + asyncMiddleware, + openapiOperationDoc, + optionalAuthenticate, + paginationValidator, + setDefaultPagination, + setDefaultSearchSort, + videoChannelsListSearchValidator, + videoChannelsSearchSortValidator +} from '../../../middlewares' +import { VideoChannelModel } from '../../../models/video/video-channel' +import { MChannelAccountDefault } from '../../../types/models' + +const searchChannelsRouter = express.Router() + +searchChannelsRouter.get('/video-channels', + openapiOperationDoc({ operationId: 'searchChannels' }), + paginationValidator, + setDefaultPagination, + videoChannelsSearchSortValidator, + setDefaultSearchSort, + optionalAuthenticate, + videoChannelsListSearchValidator, + asyncMiddleware(searchVideoChannels) +) + +// --------------------------------------------------------------------------- + +export { searchChannelsRouter } + +// --------------------------------------------------------------------------- + +function searchVideoChannels (req: express.Request, res: express.Response) { + const query: VideoChannelsSearchQuery = req.query + const search = query.search + + const parts = search.split('@') + + // Handle strings like @toto@example.com + if (parts.length === 3 && parts[0].length === 0) parts.shift() + const isWebfingerSearch = parts.length === 2 && parts.every(p => p && !p.includes(' ')) + + if (isURISearch(search) || isWebfingerSearch) return searchVideoChannelURI(search, isWebfingerSearch, res) + + // @username -> username to search in DB + if (query.search.startsWith('@')) query.search = query.search.replace(/^@/, '') + + if (isSearchIndexSearch(query)) { + return searchVideoChannelsIndex(query, res) + } + + return searchVideoChannelsDB(query, res) +} + +async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: express.Response) { + const result = await buildMutedForSearchIndex(res) + + const body = await Hooks.wrapObject(Object.assign(query, result), 'filter:api.search.video-channels.index.list.params') + + const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/video-channels' + + try { + logger.debug('Doing video channels search index request on %s.', url, { body }) + + const { body: searchIndexResult } = await doJSONRequest>(url, { method: 'POST', json: body }) + const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.video-channels.index.list.result') + + return res.json(jsonResult) + } catch (err) { + logger.warn('Cannot use search index to make video channels search.', { err }) + + return res.fail({ + status: HttpStatusCode.INTERNAL_SERVER_ERROR_500, + message: 'Cannot use search index to make video channels search' + }) + } +} + +async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: express.Response) { + const serverActor = await getServerActor() + + const apiOptions = await Hooks.wrapObject({ + actorId: serverActor.id, + search: query.search, + start: query.start, + count: query.count, + sort: query.sort + }, 'filter:api.search.video-channels.local.list.params') + + const resultList = await Hooks.wrapPromiseFun( + VideoChannelModel.searchForApi, + apiOptions, + 'filter:api.search.video-channels.local.list.result' + ) + + return res.json(getFormattedObjects(resultList.data, resultList.total)) +} + +async function searchVideoChannelURI (search: string, isWebfingerSearch: boolean, res: express.Response) { + let videoChannel: MChannelAccountDefault + let uri = search + + if (isWebfingerSearch) { + try { + uri = await loadActorUrlOrGetFromWebfinger(search) + } catch (err) { + logger.warn('Cannot load actor URL or get from webfinger.', { search, err }) + + return res.json({ total: 0, data: [] }) + } + } + + if (isUserAbleToSearchRemoteURI(res)) { + try { + const actor = await getOrCreateAPActor(uri, 'all', true, true) + videoChannel = actor.VideoChannel + } catch (err) { + logger.info('Cannot search remote video channel %s.', uri, { err }) + } + } else { + videoChannel = await VideoChannelModel.loadByUrlAndPopulateAccount(sanitizeLocalUrl(uri)) + } + + return res.json({ + total: videoChannel ? 1 : 0, + data: videoChannel ? [ videoChannel.toFormattedJSON() ] : [] + }) +} + +function sanitizeLocalUrl (url: string) { + if (!url) return '' + + // Handle alternative channel URLs + return url.replace(new RegExp('^' + WEBSERVER.URL + '/c/'), WEBSERVER.URL + '/video-channels/') +} 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 @@ +import * as express from 'express' +import { sanitizeUrl } from '@server/helpers/core-utils' +import { isUserAbleToSearchRemoteURI } from '@server/helpers/express-utils' +import { logger } from '@server/helpers/logger' +import { doJSONRequest } from '@server/helpers/requests' +import { getFormattedObjects } from '@server/helpers/utils' +import { CONFIG } from '@server/initializers/config' +import { getOrCreateAPVideoPlaylist } from '@server/lib/activitypub/playlists/get' +import { Hooks } from '@server/lib/plugins/hooks' +import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search' +import { getServerActor } from '@server/models/application/application' +import { VideoPlaylistModel } from '@server/models/video/video-playlist' +import { MVideoPlaylistFullSummary } from '@server/types/models' +import { HttpStatusCode } from '@shared/core-utils' +import { ResultList, VideoPlaylist, VideoPlaylistsSearchQuery } from '@shared/models' +import { + asyncMiddleware, + openapiOperationDoc, + optionalAuthenticate, + paginationValidator, + setDefaultPagination, + setDefaultSearchSort, + videoPlaylistsListSearchValidator, + videoPlaylistsSearchSortValidator +} from '../../../middlewares' +import { WEBSERVER } from '@server/initializers/constants' + +const searchPlaylistsRouter = express.Router() + +searchPlaylistsRouter.get('/video-playlists', + openapiOperationDoc({ operationId: 'searchPlaylists' }), + paginationValidator, + setDefaultPagination, + videoPlaylistsSearchSortValidator, + setDefaultSearchSort, + optionalAuthenticate, + videoPlaylistsListSearchValidator, + asyncMiddleware(searchVideoPlaylists) +) + +// --------------------------------------------------------------------------- + +export { searchPlaylistsRouter } + +// --------------------------------------------------------------------------- + +function searchVideoPlaylists (req: express.Request, res: express.Response) { + const query: VideoPlaylistsSearchQuery = req.query + const search = query.search + + if (isURISearch(search)) return searchVideoPlaylistsURI(search, res) + + if (isSearchIndexSearch(query)) { + return searchVideoPlaylistsIndex(query, res) + } + + return searchVideoPlaylistsDB(query, res) +} + +async function searchVideoPlaylistsIndex (query: VideoPlaylistsSearchQuery, res: express.Response) { + const result = await buildMutedForSearchIndex(res) + + const body = await Hooks.wrapObject(Object.assign(query, result), 'filter:api.search.video-playlists.index.list.params') + + const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/video-playlists' + + try { + logger.debug('Doing video playlists search index request on %s.', url, { body }) + + const { body: searchIndexResult } = await doJSONRequest>(url, { method: 'POST', json: body }) + const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.video-playlists.index.list.result') + + return res.json(jsonResult) + } catch (err) { + logger.warn('Cannot use search index to make video playlists search.', { err }) + + return res.fail({ + status: HttpStatusCode.INTERNAL_SERVER_ERROR_500, + message: 'Cannot use search index to make video playlists search' + }) + } +} + +async function searchVideoPlaylistsDB (query: VideoPlaylistsSearchQuery, res: express.Response) { + const serverActor = await getServerActor() + + const apiOptions = await Hooks.wrapObject({ + followerActorId: serverActor.id, + search: query.search, + start: query.start, + count: query.count, + sort: query.sort + }, 'filter:api.search.video-playlists.local.list.params') + + const resultList = await Hooks.wrapPromiseFun( + VideoPlaylistModel.searchForApi, + apiOptions, + 'filter:api.search.video-playlists.local.list.result' + ) + + return res.json(getFormattedObjects(resultList.data, resultList.total)) +} + +async function searchVideoPlaylistsURI (search: string, res: express.Response) { + let videoPlaylist: MVideoPlaylistFullSummary + + if (isUserAbleToSearchRemoteURI(res)) { + try { + videoPlaylist = await getOrCreateAPVideoPlaylist(search) + } catch (err) { + logger.info('Cannot search remote video playlist %s.', search, { err }) + } + } else { + videoPlaylist = await VideoPlaylistModel.loadByUrlWithAccountAndChannelSummary(sanitizeLocalUrl(search)) + } + + return res.json({ + total: videoPlaylist ? 1 : 0, + data: videoPlaylist ? [ videoPlaylist.toFormattedJSON() ] : [] + }) +} + +function sanitizeLocalUrl (url: string) { + if (!url) return '' + + // Handle alternative channel URLs + return url.replace(new RegExp('^' + WEBSERVER.URL + '/videos/watch/playlist/'), WEBSERVER.URL + '/video-playlists/') + .replace(new RegExp('^' + WEBSERVER.URL + '/w/p/'), WEBSERVER.URL + '/video-playlists/') +} 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 @@ +import * as express from 'express' +import { sanitizeUrl } from '@server/helpers/core-utils' +import { doJSONRequest } from '@server/helpers/requests' +import { CONFIG } from '@server/initializers/config' +import { WEBSERVER } from '@server/initializers/constants' +import { getOrCreateAPVideo } from '@server/lib/activitypub/videos' +import { Hooks } from '@server/lib/plugins/hooks' +import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search' +import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' +import { ResultList, Video } from '@shared/models' +import { VideosSearchQuery } from '../../../../shared/models/search' +import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils' +import { logger } from '../../../helpers/logger' +import { getFormattedObjects } from '../../../helpers/utils' +import { + asyncMiddleware, + commonVideosFiltersValidator, + openapiOperationDoc, + optionalAuthenticate, + paginationValidator, + setDefaultPagination, + setDefaultSearchSort, + videosSearchSortValidator, + videosSearchValidator +} from '../../../middlewares' +import { VideoModel } from '../../../models/video/video' +import { MVideoAccountLightBlacklistAllFiles } from '../../../types/models' + +const searchVideosRouter = express.Router() + +searchVideosRouter.get('/videos', + openapiOperationDoc({ operationId: 'searchVideos' }), + paginationValidator, + setDefaultPagination, + videosSearchSortValidator, + setDefaultSearchSort, + optionalAuthenticate, + commonVideosFiltersValidator, + videosSearchValidator, + asyncMiddleware(searchVideos) +) + +// --------------------------------------------------------------------------- + +export { searchVideosRouter } + +// --------------------------------------------------------------------------- + +function searchVideos (req: express.Request, res: express.Response) { + const query: VideosSearchQuery = req.query + const search = query.search + + if (isURISearch(search)) { + return searchVideoURI(search, res) + } + + if (isSearchIndexSearch(query)) { + return searchVideosIndex(query, res) + } + + return searchVideosDB(query, res) +} + +async function searchVideosIndex (query: VideosSearchQuery, res: express.Response) { + const result = await buildMutedForSearchIndex(res) + + let body: VideosSearchQuery = Object.assign(query, result) + + // Use the default instance NSFW policy if not specified + if (!body.nsfw) { + const nsfwPolicy = res.locals.oauth + ? res.locals.oauth.token.User.nsfwPolicy + : CONFIG.INSTANCE.DEFAULT_NSFW_POLICY + + body.nsfw = nsfwPolicy === 'do_not_list' + ? 'false' + : 'both' + } + + body = await Hooks.wrapObject(body, 'filter:api.search.videos.index.list.params') + + const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/videos' + + try { + logger.debug('Doing videos search index request on %s.', url, { body }) + + const { body: searchIndexResult } = await doJSONRequest>(url, { method: 'POST', json: body }) + const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.videos.index.list.result') + + return res.json(jsonResult) + } catch (err) { + logger.warn('Cannot use search index to make video search.', { err }) + + return res.fail({ + status: HttpStatusCode.INTERNAL_SERVER_ERROR_500, + message: 'Cannot use search index to make video search' + }) + } +} + +async function searchVideosDB (query: VideosSearchQuery, res: express.Response) { + const apiOptions = await Hooks.wrapObject(Object.assign(query, { + includeLocalVideos: true, + nsfw: buildNSFWFilter(res, query.nsfw), + filter: query.filter, + user: res.locals.oauth ? res.locals.oauth.token.User : undefined + }), 'filter:api.search.videos.local.list.params') + + const resultList = await Hooks.wrapPromiseFun( + VideoModel.searchAndPopulateAccountAndServer, + apiOptions, + 'filter:api.search.videos.local.list.result' + ) + + return res.json(getFormattedObjects(resultList.data, resultList.total)) +} + +async function searchVideoURI (url: string, res: express.Response) { + let video: MVideoAccountLightBlacklistAllFiles + + // Check if we can fetch a remote video with the URL + if (isUserAbleToSearchRemoteURI(res)) { + try { + const syncParam = { + likes: false, + dislikes: false, + shares: false, + comments: false, + thumbnail: true, + refreshVideo: false + } + + const result = await getOrCreateAPVideo({ videoObject: url, syncParam }) + video = result ? result.video : undefined + } catch (err) { + logger.info('Cannot search remote video %s.', url, { err }) + } + } else { + video = await VideoModel.loadByUrlAndPopulateAccount(sanitizeLocalUrl(url)) + } + + return res.json({ + total: video ? 1 : 0, + data: video ? [ video.toFormattedJSON() ] : [] + }) +} + +function sanitizeLocalUrl (url: string) { + if (!url) return '' + + // Handle alternative video URLs + return url.replace(new RegExp('^' + WEBSERVER.URL + '/w/'), WEBSERVER.URL + '/videos/watch/') +} -- cgit v1.2.3