diff --git a/PeerTube b/PeerTube index 6b43594..cffa06f 160000 --- a/PeerTube +++ b/PeerTube @@ -1 +1 @@ -Subproject commit 6b4359476c462ea178c99b0a04349f553ddb8d9d +Subproject commit cffa06fd28bbfb03980bc7d151cfd93130c3daaf diff --git a/client/src/components/PlaylistResult.vue b/client/src/components/PlaylistResult.vue new file mode 100644 index 0000000..80fa6da --- /dev/null +++ b/client/src/components/PlaylistResult.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/client/src/components/VideoResult.vue b/client/src/components/VideoResult.vue index 8adb806..b40518b 100644 --- a/client/src/components/VideoResult.vue +++ b/client/src/components/VideoResult.vue @@ -163,7 +163,7 @@ return this.video.previewUrl }, renderMarkdown(markdown: string) { - return renderMarkdown(markdown) + return renderMarkdown(markdown) } } }) diff --git a/client/src/scss/_variables.scss b/client/src/scss/_variables.scss index 9214144..c5f7217 100644 --- a/client/src/scss/_variables.scss +++ b/client/src/scss/_variables.scss @@ -12,8 +12,8 @@ $small-screen: 700px; $primary: $orange; $secondary: $grey; -$thumbnail-width: 223px; -$thumbnail-height: 122px; +$thumbnail-width: 280px; +$thumbnail-height: 153px; $small-font-size: 12px; diff --git a/client/src/shared/markdown-render.ts b/client/src/shared/markdown-render.ts index 8b9b64c..61b3e8d 100644 --- a/client/src/shared/markdown-render.ts +++ b/client/src/shared/markdown-render.ts @@ -16,6 +16,7 @@ for (const rule of TEXT_RULES) { } export function renderMarkdown(markdown: string) { - let html = markdownIt.render(markdown); - return html; + if (!markdown) return '' + + return markdownIt.render(markdown) } diff --git a/client/src/shared/search.ts b/client/src/shared/search.ts index a6841d1..78866ad 100644 --- a/client/src/shared/search.ts +++ b/client/src/shared/search.ts @@ -1,13 +1,16 @@ import axios from 'axios' +import { VideoPlaylistsSearchQuery } from '../../../PeerTube/shared/models' import { ResultList } from '../../../PeerTube/shared/models/result-list.model' import { VideoChannelsSearchQuery } from '../../../PeerTube/shared/models/search/video-channels-search-query.model' import { VideosSearchQuery } from '../../../PeerTube/shared/models/search/videos-search-query.model' import { EnhancedVideoChannel } from '../../../server/types/channel.model' import { EnhancedVideo } from '../../../server/types/video.model' +import { EnhancedPlaylist } from '../../../server/types/playlist.model' import { buildApiUrl } from './utils' const baseVideosPath = '/api/v1/search/videos' const baseVideoChannelsPath = '/api/v1/search/video-channels' +const baseVideoPlaylistsPath = '/api/v1/search/video-playlists' function searchVideos (params: VideosSearchQuery) { const options = { @@ -31,7 +34,19 @@ function searchVideoChannels (params: VideoChannelsSearchQuery) { .then(res => res.data) } +function searchVideoPlaylists (params: VideoPlaylistsSearchQuery) { + const options = { + params + } + + if (params.search) Object.assign(options.params, { search: params.search }) + + return axios.get>(buildApiUrl(baseVideoPlaylistsPath), options) + .then(res => res.data) +} + export { searchVideos, - searchVideoChannels + searchVideoChannels, + searchVideoPlaylists } diff --git a/client/src/views/Search.vue b/client/src/views/Search.vue index 6fd3d2d..2e69b52 100644 --- a/client/src/views/Search.vue +++ b/client/src/views/Search.vue @@ -20,7 +20,7 @@

- Search for your favorite videos and channels on %{instancesCount} PeerTube websites indexed by %{indexName}! + Search for your favorite videos, channels and playlists on %{instancesCount} PeerTube websites indexed by %{indexName}!

@@ -213,7 +213,9 @@
- + + +
@@ -442,7 +444,8 @@ import SearchWarning from '../components/SearchWarning.vue' import VideoResult from '../components/VideoResult.vue' import ChannelResult from '../components/ChannelResult.vue' - import { searchVideos, searchVideoChannels } from '../shared/search' + import PlaylistResult from '../components/PlaylistResult.vue' + import { searchVideos, searchVideoChannels, searchVideoPlaylists } from '../shared/search' import { getConfig } from '../shared/config' import { pageToAPIParams, durationRangeToAPIParams, publishedDateRangeToAPIParams, extractTagsFromQuery, buildApiUrl } from '../shared/utils' import { SearchUrl } from '../models' @@ -450,9 +453,10 @@ import { EnhancedVideoChannel } from '../../../server/types/channel.model' import VueTagsInput from '@johmun/vue-tags-input' import Pagination from '../components/Pagination.vue' - import { VideoChannelsSearchQuery, ResultList } from '../../../PeerTube/shared/models' + import { VideoChannelsSearchQuery, ResultList, VideosSearchQuery } from '../../../PeerTube/shared/models' import Nprogress from 'nprogress' - import { VideosSearchQuery } from '../../../server/types/video-search.model' + import { EnhancedPlaylist } from '../../../server/types/playlist.model' + import { PlaylistsSearchQuery } from '../../../server/types/search-query/playlist-search.model' export default Vue.extend({ components: { @@ -460,6 +464,7 @@ 'search-warning': SearchWarning, 'video-result': VideoResult, 'channel-result': ChannelResult, + 'playlist-result': PlaylistResult, 'vue-tags-input': VueTagsInput, 'pagination': Pagination }, @@ -471,11 +476,12 @@ indexName: '', instancesCount: 0, indexedInstancesUrl: '', - results: [] as (EnhancedVideo | EnhancedVideoChannel)[], + results: [] as (EnhancedVideo | EnhancedVideoChannel | EnhancedPlaylist)[], resultsCount: null as number, channelsCount: null as number, videosCount: null as number, + playlistsCount: null as number, searchedTerm: '', @@ -483,6 +489,7 @@ pages: [], resultsPerVideosPage: 10, resultsPerChannelsPage: 3, + resultsPerPlaylistsPage: 3, displayFilters: false, oldQuery: '', @@ -553,7 +560,7 @@ }, inputPlaceholder () { - return this.$gettext('Keyword, channel, video, etc.') + return this.$gettext('Keyword, channel, video, playlist, etc.') }, tagsPlaceholder () { @@ -691,6 +698,7 @@ this.currentPage = 1 this.channelsCount = null this.videosCount = null + this.playlistsCount = null this.resultsCount = null this.updateUrl() @@ -705,9 +713,10 @@ Nprogress.start() try { - const [ videosResult, channelsResult ] = await Promise.all([ + const [ videosResult, channelsResult, playlistsResult ] = await Promise.all([ this.searchVideos(), - this.searchChannels() + this.searchChannels(), + this.searchPlaylists() ]) Nprogress.done() @@ -716,9 +725,11 @@ this.channelsCount = channelsResult.total this.videosCount = videosResult.total + this.playlistsCount = playlistsResult.total - this.resultsCount = videosResult.total + channelsResult.total - this.results = channelsResult.data.concat(videosResult.data) + this.resultsCount = videosResult.total + channelsResult.total + playlistsResult.total + this.results = channelsResult.data.concat(playlistsResult.data) + .concat(videosResult.data) if (this.formSort === '-match') { this.results.sort((r1, r2) => { @@ -739,16 +750,30 @@ } }, - isVideo (result: EnhancedVideo | EnhancedVideoChannel) { + isVideo (result: EnhancedVideo | EnhancedVideoChannel | EnhancedPlaylist) { if ((result as EnhancedVideo).language) return true return false }, - getResultKey (result: EnhancedVideo | EnhancedVideoChannel) { - if (this.isVideo(result)) return (result as EnhancedVideo).uuid + isPlaylist (result: EnhancedVideo | EnhancedVideoChannel | EnhancedPlaylist) { + if ((result as EnhancedPlaylist).videosLength !== undefined) return true - return result.id + (result as EnhancedVideoChannel).host + return false + }, + + isChannel (result: EnhancedVideo | EnhancedVideoChannel | EnhancedPlaylist) { + if ((result as EnhancedVideoChannel).followingCount !== undefined) return true + + return false + }, + + getResultKey (result: EnhancedVideo | EnhancedVideoChannel | EnhancedPlaylist) { + if (this.isVideo(result)) return (result as EnhancedVideo).uuid + if (this.isChannel(result)) return result.id + (result as EnhancedVideoChannel).host + if (this.isPlaylist(result)) return (result as EnhancedPlaylist).uuid + + throw new Error('Unknown result') }, updateUrl () { @@ -864,6 +889,22 @@ } as VideoChannelsSearchQuery }, + buildPlaylistSearchQuery () { + const { start, count } = pageToAPIParams(this.currentPage, this.resultsPerChannelsPage) + + let sort: string + if (this.formSort === '-matched') sort = '-matched' + else if (this.formSort === '-publishedAt') sort = '-createdAt' + else if (this.formSort === 'publishedAt') sort = 'createdAt' + + return { + search: this.formSearch, + start, + sort, + count + } as PlaylistsSearchQuery + }, + searchVideos (): Promise> { if (!this.formSearch) { return Promise.resolve({ data: [], total: 0 }) @@ -879,7 +920,7 @@ }, searchChannels (): Promise> { - if (!this.formSearch || this.isChannelSearchDisabled()) { + if (!this.formSearch || this.isChannelOrPlaylistSearchDisabled()) { return Promise.resolve({ data: [], total: 0 }) } @@ -892,6 +933,20 @@ return searchVideoChannels(query) }, + searchPlaylists (): Promise> { + if (!this.formSearch || this.isChannelOrPlaylistSearchDisabled()) { + return Promise.resolve({ data: [], total: 0 }) + } + + if (!this.hasStillPlaylistsResult()) { + return Promise.resolve({ data: [], total: this.channelsCount }) + } + + const query = this.buildChannelSearchQuery() + + return searchVideoPlaylists(query) + }, + hasStillChannelsResult () { // Not searched yet if (this.channelsCount === null) return true @@ -899,6 +954,13 @@ return this.getChannelsMaxPage() >= this.currentPage }, + hasStillPlaylistsResult () { + // Not searched yet + if (this.playlistsCount === null) return true + + return this.getPlaylistsMaxPage() >= this.currentPage + }, + hasStillMoreVideosResults () { // Not searched yet if (this.videosCount === null) return true @@ -910,13 +972,17 @@ return Math.ceil(this.channelsCount / this.resultsPerChannelsPage) }, + getPlaylistsMaxPage () { + return Math.ceil(this.playlistsCount / this.resultsPerPlaylistsPage) + }, + getVideosMaxPage () { return Math.ceil(this.videosCount / this.resultsPerVideosPage) }, getMaxPage () { // Limit to 10 pages - return Math.min(10, Math.max(this.getChannelsMaxPage(), this.getVideosMaxPage())) + return Math.min(10, Math.max(this.getPlaylistsMaxPage(), this.getChannelsMaxPage(), this.getVideosMaxPage())) }, buildPages () { @@ -964,7 +1030,7 @@ return count }, - isChannelSearchDisabled () { + isChannelOrPlaylistSearchDisabled () { return this.countActiveFilters() > 0 }, diff --git a/config/default.yaml b/config/default.yaml index 19cd2a3..74b07c1 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -93,7 +93,7 @@ playlists-search: # See https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html for more information # If boost == 0, the field will not be part of the search search-fields: - name: + display-name: boost: 5 description: boost: 1 diff --git a/config/test.yaml b/config/test.yaml index 7a82c14..2a50d69 100644 --- a/config/test.yaml +++ b/config/test.yaml @@ -2,6 +2,7 @@ elastic-search: indexes: videos: 'peertube-index-videos-test1' channels: 'peertube-index-channels-test1' + playlists: 'peertube-index-playlists-test1' search-instance: name_image: '/theme/framasoft/img/title.svg' diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index d2ea98e..cb8c51a 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -89,9 +89,9 @@ const CONFIG = { }, PLAYLISTS_SEARCH: { SEARCH_FIELDS: { - NAME: { - FIELD_NAME: 'name', - BOOST: config.get('playlists-search.search-fields.name.boost') + DISPLAY_NAME: { + FIELD_NAME: 'displayName', + BOOST: config.get('playlists-search.search-fields.display-name.boost') }, DESCRIPTION: { FIELD_NAME: 'description', @@ -124,14 +124,13 @@ const SORTABLE_COLUMNS = { const PAGINATION_COUNT_DEFAULT = 20 const SCHEDULER_INTERVALS_MS = { - videosIndexer: 60000 * 60 * 24 // 24 hours + indexation: 60000 * 60 * 24 // 24 hours } const INDEXER_COUNT = 10 const INDEXER_LIMIT = 500000 -const INDEXER_CONCURRENCY = 3 - +const INDEXER_HOST_CONCURRENCY = 3 const INDEXER_QUEUE_CONCURRENCY = 3 const REQUESTS = { @@ -167,7 +166,7 @@ function buildMultiMatchFields (fields: { [name: string]: { BOOST: number, FIELD } if (isTestInstance()) { - SCHEDULER_INTERVALS_MS.videosIndexer = 1000 * 60 * 5 // 5 minutes + SCHEDULER_INTERVALS_MS.indexation = 1000 * 60 * 5 // 5 minutes } export { @@ -179,7 +178,7 @@ export { SORTABLE_COLUMNS, INDEXER_QUEUE_CONCURRENCY, SCHEDULER_INTERVALS_MS, - INDEXER_CONCURRENCY, + INDEXER_HOST_CONCURRENCY, INDEXER_COUNT, INDEXER_LIMIT, REQUESTS, diff --git a/server/lib/schedulers/indexation-scheduler.ts b/server/lib/schedulers/indexation-scheduler.ts index 69f0591..6b7eb66 100644 --- a/server/lib/schedulers/indexation-scheduler.ts +++ b/server/lib/schedulers/indexation-scheduler.ts @@ -2,7 +2,7 @@ import * as Bluebird from 'bluebird' import { IndexablePlaylist } from 'server/types/playlist.model' import { inspect } from 'util' import { logger } from '../../helpers/logger' -import { INDEXER_CONCURRENCY, INDEXER_COUNT, INDEXER_LIMIT, SCHEDULER_INTERVALS_MS } from '../../initializers/constants' +import { INDEXER_HOST_CONCURRENCY, INDEXER_COUNT, INDEXER_LIMIT, SCHEDULER_INTERVALS_MS } from '../../initializers/constants' import { IndexableVideo } from '../../types/video.model' import { buildInstanceHosts } from '../elastic-search/elastic-search-instances' import { ChannelIndexer } from '../indexers/channel-indexer' @@ -15,7 +15,7 @@ export class IndexationScheduler extends AbstractScheduler { private static instance: IndexationScheduler - protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.videosIndexer + protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.indexation private indexedHosts: string[] = [] @@ -68,7 +68,7 @@ export class IndexationScheduler extends AbstractScheduler { console.error(inspect(err, { depth: 10 })) logger.warn({ err: inspect(err) }, 'Cannot index videos from %s.', host) } - }, { concurrency: INDEXER_CONCURRENCY }) + }, { concurrency: INDEXER_HOST_CONCURRENCY }) for (const o of this.indexers) { await o.refreshIndex() @@ -91,10 +91,10 @@ export class IndexationScheduler extends AbstractScheduler { logger.debug('Getting video results from %s (from = %d).', host, start) videos = await getVideos(host, start) - start += videos.length - logger.debug('Got %d video results from %s (from = %d).', videos.length, host, start) + start += videos.length + if (videos.length !== 0) { const { created } = await this.videoIndexer.indexElements(videos) @@ -139,10 +139,10 @@ export class IndexationScheduler extends AbstractScheduler { logger.debug('Getting playlist results from %s (from = %d, channelHandle = %s).', host, start, channelHandle) playlists = await getPlaylistsOf(host, channelHandle, start) - start += playlists.length - logger.debug('Got %d playlist results from %s (from = %d, channelHandle = %s).', playlists.length, host, start, channelHandle) + start += playlists.length + if (playlists.length !== 0) { await this.playlistIndexer.indexElements(playlists)