Add playlist in client

This commit is contained in:
Chocobozzz 2021-06-24 16:53:43 +02:00
parent db36a2fb6a
commit 20f9f0f708
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
11 changed files with 268 additions and 41 deletions

@ -1 +1 @@
Subproject commit 6b4359476c462ea178c99b0a04349f553ddb8d9d
Subproject commit cffa06fd28bbfb03980bc7d151cfd93130c3daaf

View File

@ -0,0 +1,145 @@
<template>
<div class="playlist root-result">
<div class="thumbnail">
<a class="img" :title="watchMessage" target="_blank" rel="nofollow noreferrer noopener" v-bind:href="playlist.url">
<img v-bind:src="playlist.thumbnailUrl" alt="">
<span class="videos-length">{{ videosLengthLabel }}</span>
</a>
</div>
<div class="information">
<h5 class="title">
<a :title="watchMessage" target="_blank" rel="nofollow noreferrer noopener" v-bind:href="playlist.url">{{ playlist.displayName }}</a>
</h5>
<div class="description" v-html="renderMarkdown(playlist.description)"></div>
<div class="metadatas">
<div class="by-account">
<label v-translate>Created by</label>
<actor-miniature type="account" v-bind:actor="playlist.ownerAccount"></actor-miniature>
</div>
<div class="by-channel">
<label v-translate>In</label>
<actor-miniature type="channel" v-bind:actor="playlist.videoChannel"></actor-miniature>
</div>
<div class="publishedAt">
<label v-translate>Updated on</label>
<div class="value">{{ updateDate }}</div>
</div>
</div>
<div class="button">
<a class="button-link" target="_blank" rel="nofollow noreferrer noopener" v-bind:href="playlist.url">
{{ watchMessage }}
</a>
</div>
</div>
</div>
</template>
<style lang="scss">
@import '../scss/_variables';
.playlist {
.thumbnail {
margin-right: 20px;
--thumbnail-width: #{$thumbnail-width};
--thumbnail-height: #{$thumbnail-height};
// For the videos length overlay
.img {
position: relative;
display: inline-block;
width: var(--thumbnail-width);
height: var(--thumbnail-height);
border-radius: 3px;
overflow: hidden;
}
.videos-length {
position: absolute;
right: 0;
bottom: 0;
display: flex;
align-items: center;
padding: 0 10px;
height: 100%;
color: #fff;
background-color: rgba(0,0,0,.7);
font-size: 14px;
font-weight: 600;
}
img {
width: 100%;
height: 100%;
}
@media screen and (max-width: $small-view) {
--thumbnail-width: calc(100% + 10px);
--thumbnail-height: auto;
img {
border-radius: 0;
}
}
}
}
</style>
<script lang="ts">
import Vue, { PropType } from 'vue'
import ActorMiniature from './ActorMiniature.vue'
import { VideoPlaylist } from '../../../PeerTube/shared/models'
import { renderMarkdown } from '../shared/markdown-render'
export default Vue.extend({
components: {
'actor-miniature': ActorMiniature
},
props: {
playlist: Object as PropType<VideoPlaylist>
},
computed: {
host () {
const url = this.playlist.url
return new URL(url as string).host
},
updateDate () {
return new Date(this.playlist.updatedAt).toLocaleDateString()
},
watchMessage () {
return this.$gettextInterpolate(this.$gettext('Watch the playlist on %{host}'), { host: this.host })
},
videosLengthLabel () {
return this.$gettextInterpolate(this.$gettext('%{videosLength} videos'), { videosLength: this.playlist.videosLength })
}
},
methods: {
renderMarkdown(markdown: string) {
return renderMarkdown(markdown)
}
}
})
</script>

View File

@ -163,7 +163,7 @@
return this.video.previewUrl
},
renderMarkdown(markdown: string) {
return renderMarkdown(markdown)
return renderMarkdown(markdown)
}
}
})

View File

@ -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;

View File

@ -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)
}

View File

@ -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<ResultList<EnhancedPlaylist>>(buildApiUrl(baseVideoPlaylistsPath), options)
.then(res => res.data)
}
export {
searchVideos,
searchVideoChannels
searchVideoChannels,
searchVideoPlaylists
}

View File

@ -20,7 +20,7 @@
</form>
<h3 v-if="!searchDone" v-bind:class="{ 'none-opacity': !instancesCount }" v-translate="{ instancesCount: instancesCount, indexedInstancesUrl: indexedInstancesUrl, indexName: indexName }">
Search for your favorite videos and channels on <a href="%{indexedInstancesUrl}" target="_blank">%{instancesCount} PeerTube websites</a> indexed by %{indexName}!
Search for your favorite videos, channels and playlists on <a href="%{indexedInstancesUrl}" target="_blank">%{instancesCount} PeerTube websites</a> indexed by %{indexName}!
</h3>
<search-warning class="search-warning" v-bind:indexName="indexName" v-bind:highlight="!searchDone"></search-warning>
@ -213,7 +213,9 @@
<div v-for="result in results" :key="getResultKey(result)">
<video-result v-if="isVideo(result)" v-bind:video="result"></video-result>
<channel-result v-else v-bind:channel="result"></channel-result>
<channel-result v-else-if="isChannel(result)" v-bind:channel="result"></channel-result>
<playlist-result v-else v-bind:playlist="result"></playlist-result>
</div>
<pagination v-bind:maxPage="getMaxPage()" v-bind:searchDone="searchDone" v-model="currentPage" v-bind:pages="pages"></pagination>
@ -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<ResultList<EnhancedVideo>> {
if (!this.formSearch) {
return Promise.resolve({ data: [], total: 0 })
@ -879,7 +920,7 @@
},
searchChannels (): Promise<ResultList<EnhancedVideoChannel>> {
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<ResultList<EnhancedPlaylist>> {
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
},

View File

@ -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

View File

@ -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'

View File

@ -89,9 +89,9 @@ const CONFIG = {
},
PLAYLISTS_SEARCH: {
SEARCH_FIELDS: {
NAME: {
FIELD_NAME: 'name',
BOOST: config.get<number>('playlists-search.search-fields.name.boost')
DISPLAY_NAME: {
FIELD_NAME: 'displayName',
BOOST: config.get<number>('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,

View File

@ -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)