From 37a44fc915eef2140e22ceb96aba6b6eb2509007 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 17 Jun 2021 16:02:38 +0200 Subject: [PATCH] Add ability to search playlists --- .../my-video-playlist-elements.component.ts | 2 +- .../app/+search/channel-lazy-load.resolver.ts | 35 --- .../src/app/+search/search-routing.module.ts | 10 +- client/src/app/+search/search.component.html | 9 +- client/src/app/+search/search.component.ts | 154 +++++---- client/src/app/+search/search.module.ts | 10 +- .../abstract-lazy-load.resolver.ts} | 21 +- .../shared/channel-lazy-load.resolver.ts | 24 ++ client/src/app/+search/shared/index.ts | 4 + .../shared/playlist-lazy-load.resolver.ts | 24 ++ .../shared/video-lazy-load.resolver.ts | 24 ++ .../header/search-typeahead.component.html | 2 +- .../app/shared/shared-main/angular/index.ts | 1 + .../shared-main/angular/link.component.html | 11 + .../shared-main/angular/link.component.scss | 7 + .../shared-main/angular/link.component.ts | 17 + .../shared/shared-main/shared-main.module.ts | 7 +- .../shared/shared-search/search.service.ts | 44 ++- .../video-miniature.component.html | 13 +- .../video-miniature.component.ts | 5 +- .../video-playlist-miniature.component.html | 15 +- .../video-playlist-miniature.component.scss | 3 + .../video-playlist-miniature.component.ts | 47 ++- .../video-playlist.model.ts | 31 +- client/src/sass/include/_mixins.scss | 1 + client/src/types/link.type.ts | 1 + server/controllers/activitypub/client.ts | 3 +- server/controllers/api/search.ts | 294 ------------------ server/controllers/api/search/index.ts | 16 + .../api/search/search-video-channels.ts | 150 +++++++++ .../api/search/search-video-playlists.ts | 129 ++++++++ .../controllers/api/search/search-videos.ts | 153 +++++++++ server/controllers/api/video-channel.ts | 4 +- server/controllers/api/video-playlist.ts | 8 +- .../custom-validators/activitypub/playlist.ts | 7 +- server/initializers/constants.ts | 1 + server/lib/activitypub/actors/get.ts | 2 +- .../activitypub/playlists/create-update.ts | 34 +- server/lib/activitypub/playlists/get.ts | 35 +++ server/lib/activitypub/playlists/index.ts | 1 + server/lib/activitypub/playlists/refresh.ts | 13 +- .../shared/object-to-model-attributes.ts | 6 +- .../lib/activitypub/process/process-create.ts | 2 +- .../lib/activitypub/process/process-update.ts | 2 +- server/lib/activitypub/videos/get.ts | 7 +- .../handlers/activitypub-http-fetcher.ts | 8 +- server/lib/search.ts | 50 +++ server/middlewares/validators/search.ts | 9 +- server/middlewares/validators/sort.ts | 3 + .../validators/videos/video-channels.ts | 13 + .../shared/abstract-videos-query-builder.ts | 2 +- server/models/video/video-channel.ts | 4 +- server/models/video/video-playlist.ts | 84 ++++- server/tests/api/check-params/search.ts | 25 ++ server/tests/api/search/index.ts | 6 +- .../search-activitypub-video-channels.ts | 16 + .../search-activitypub-video-playlists.ts | 212 +++++++++++++ .../api/search/search-activitypub-videos.ts | 37 ++- server/tests/api/search/search-index.ts | 62 +++- server/tests/api/search/search-playlists.ts | 128 ++++++++ .../languages/fr.json | 0 .../languages/it.json | 0 .../main.js | 0 .../package.json | 4 +- .../fixtures/peertube-plugin-test/main.js | 4 + server/tests/plugins/filter-hooks.ts | 24 +- server/tests/plugins/translations.ts | 8 +- server/types/models/video/video-playlist.ts | 4 +- shared/extra-utils/index.ts | 2 + shared/extra-utils/search/video-playlists.ts | 36 +++ .../plugins/client/client-hook.model.ts | 5 +- .../plugins/server/server-hook.model.ts | 4 + shared/models/search/index.ts | 3 +- .../video-channels-search-query.model.ts | 2 +- .../video-playlists-search-query.model.ts | 9 + shared/models/server/job.model.ts | 1 - .../server/peertube-problem-document.model.ts | 2 +- .../videos/playlist/video-playlist.model.ts | 4 + support/doc/api/openapi.yaml | 41 +++ 79 files changed, 1652 insertions(+), 549 deletions(-) delete mode 100644 client/src/app/+search/channel-lazy-load.resolver.ts rename client/src/app/+search/{video-lazy-load.resolver.ts => shared/abstract-lazy-load.resolver.ts} (55%) create mode 100644 client/src/app/+search/shared/channel-lazy-load.resolver.ts create mode 100644 client/src/app/+search/shared/index.ts create mode 100644 client/src/app/+search/shared/playlist-lazy-load.resolver.ts create mode 100644 client/src/app/+search/shared/video-lazy-load.resolver.ts create mode 100644 client/src/app/shared/shared-main/angular/link.component.html create mode 100644 client/src/app/shared/shared-main/angular/link.component.scss create mode 100644 client/src/app/shared/shared-main/angular/link.component.ts create mode 100644 client/src/types/link.type.ts delete mode 100644 server/controllers/api/search.ts 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 create mode 100644 server/lib/activitypub/playlists/get.ts create mode 100644 server/lib/search.ts create mode 100644 server/tests/api/search/search-activitypub-video-playlists.ts create mode 100644 server/tests/api/search/search-playlists.ts rename server/tests/fixtures/{peertube-plugin-test-two => peertube-plugin-test-filter-translations}/languages/fr.json (100%) rename server/tests/fixtures/{peertube-plugin-test-two => peertube-plugin-test-filter-translations}/languages/it.json (100%) rename server/tests/fixtures/{peertube-plugin-test-two => peertube-plugin-test-filter-translations}/main.js (100%) rename server/tests/fixtures/{peertube-plugin-test-two => peertube-plugin-test-filter-translations}/package.json (79%) create mode 100644 shared/extra-utils/search/video-playlists.ts create mode 100644 shared/models/search/video-playlists-search-query.model.ts diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.ts b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.ts index a8fdf6e29..86fe70710 100644 --- a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.ts +++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.ts @@ -187,7 +187,7 @@ export class MyVideoPlaylistElementsComponent implements OnInit, OnDestroy { // Reload playlist thumbnail if the first element changed const newFirst = this.findFirst() if (oldFirst && newFirst && oldFirst.id !== newFirst.id) { - this.playlist.refreshThumbnail() + this.loadPlaylistInfo() } } diff --git a/client/src/app/+search/channel-lazy-load.resolver.ts b/client/src/app/+search/channel-lazy-load.resolver.ts deleted file mode 100644 index d9f7ec901..000000000 --- a/client/src/app/+search/channel-lazy-load.resolver.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { map } from 'rxjs/operators' -import { Injectable } from '@angular/core' -import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router' -import { SearchService } from '@app/shared/shared-search' - -@Injectable() -export class ChannelLazyLoadResolver implements Resolve { - constructor ( - private router: Router, - private searchService: SearchService - ) { } - - resolve (route: ActivatedRouteSnapshot) { - const url = route.params.url - - if (!url) { - console.error('Could not find url param.', { params: route.params }) - return this.router.navigateByUrl('/404') - } - - return this.searchService.searchVideoChannels({ search: url }) - .pipe( - map(result => { - if (result.data.length !== 1) { - console.error('Cannot find result for this URL') - return this.router.navigateByUrl('/404') - } - - const channel = result.data[0] - - return this.router.navigateByUrl('/video-channels/' + channel.nameWithHost) - }) - ) - } -} diff --git a/client/src/app/+search/search-routing.module.ts b/client/src/app/+search/search-routing.module.ts index 0d778af0d..5d00aae13 100644 --- a/client/src/app/+search/search-routing.module.ts +++ b/client/src/app/+search/search-routing.module.ts @@ -1,8 +1,7 @@ import { NgModule } from '@angular/core' import { RouterModule, Routes } from '@angular/router' -import { ChannelLazyLoadResolver } from './channel-lazy-load.resolver' import { SearchComponent } from './search.component' -import { VideoLazyLoadResolver } from './video-lazy-load.resolver' +import { ChannelLazyLoadResolver, PlaylistLazyLoadResolver, VideoLazyLoadResolver } from './shared' const searchRoutes: Routes = [ { @@ -27,6 +26,13 @@ const searchRoutes: Routes = [ resolve: { data: ChannelLazyLoadResolver } + }, + { + path: 'lazy-load-playlist', + component: SearchComponent, + resolve: { + data: PlaylistLazyLoadResolver + } } ] diff --git a/client/src/app/+search/search.component.html b/client/src/app/+search/search.component.html index 130be75fc..b28abca6a 100644 --- a/client/src/app/+search/search.component.html +++ b/client/src/app/+search/search.component.html @@ -59,10 +59,17 @@
+ +
+ +
diff --git a/client/src/app/+search/search.component.ts b/client/src/app/+search/search.component.ts index a4ab7a5b1..fdf9b7cc0 100644 --- a/client/src/app/+search/search.component.ts +++ b/client/src/app/+search/search.component.ts @@ -1,11 +1,13 @@ import { forkJoin, of, Subscription } from 'rxjs' +import { LinkType } from 'src/types/link.type' import { Component, OnDestroy, OnInit } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' -import { AuthService, ComponentPagination, HooksService, MetaService, Notifier, ServerService, User, UserService } from '@app/core' +import { AuthService, HooksService, MetaService, Notifier, ServerService, User, UserService } from '@app/core' import { immutableAssign } from '@app/helpers' import { Video, VideoChannel } from '@app/shared/shared-main' import { AdvancedSearch, SearchService } from '@app/shared/shared-search' -import { MiniatureDisplayOptions, VideoLinkType } from '@app/shared/shared-video-miniature' +import { MiniatureDisplayOptions } from '@app/shared/shared-video-miniature' +import { VideoPlaylist } from '@app/shared/shared-video-playlist' import { HTMLServerConfig, SearchTargetType } from '@shared/models' @Component({ @@ -16,10 +18,9 @@ import { HTMLServerConfig, SearchTargetType } from '@shared/models' export class SearchComponent implements OnInit, OnDestroy { results: (Video | VideoChannel)[] = [] - pagination: ComponentPagination = { + pagination = { currentPage: 1, - itemsPerPage: 10, // Only for videos, use another variable for channels - totalItems: null + totalItems: null as number } advancedSearch: AdvancedSearch = new AdvancedSearch() isSearchFilterCollapsed = true @@ -45,6 +46,11 @@ export class SearchComponent implements OnInit, OnDestroy { private firstSearch = true private channelsPerPage = 2 + private playlistsPerPage = 2 + private videosPerPage = 10 + + private hasMoreResults = true + private isSearching = false private lastSearchTarget: SearchTargetType @@ -104,77 +110,62 @@ export class SearchComponent implements OnInit, OnDestroy { if (this.subActivatedRoute) this.subActivatedRoute.unsubscribe() } - isVideoChannel (d: VideoChannel | Video): d is VideoChannel { + isVideoChannel (d: VideoChannel | Video | VideoPlaylist): d is VideoChannel { return d instanceof VideoChannel } - isVideo (v: VideoChannel | Video): v is Video { + isVideo (v: VideoChannel | Video | VideoPlaylist): v is Video { return v instanceof Video } + isPlaylist (v: VideoChannel | Video | VideoPlaylist): v is VideoPlaylist { + return v instanceof VideoPlaylist + } + isUserLoggedIn () { return this.authService.isLoggedIn() } - getVideoLinkType (): VideoLinkType { - if (this.advancedSearch.searchTarget === 'search-index') { - const remoteUriConfig = this.serverConfig.search.remoteUri - - // Redirect on the external instance if not allowed to fetch remote data - if ((!this.isUserLoggedIn() && !remoteUriConfig.anonymous) || !remoteUriConfig.users) { - return 'external' - } - - return 'lazy-load' - } - - return 'internal' - } - search () { + this.isSearching = true + forkJoin([ - this.getVideosObs(), - this.getVideoChannelObs() - ]).subscribe( - ([videosResult, videoChannelsResult]) => { - this.results = this.results - .concat(videoChannelsResult.data) - .concat(videosResult.data) - - this.pagination.totalItems = videosResult.total + videoChannelsResult.total - this.lastSearchTarget = this.advancedSearch.searchTarget - - // Focus on channels if there are no enough videos - if (this.firstSearch === true && videosResult.data.length < this.pagination.itemsPerPage) { - this.resetPagination() - this.firstSearch = false - - this.channelsPerPage = 10 - this.search() - } - - this.firstSearch = false - }, - - err => { - if (this.advancedSearch.searchTarget !== 'search-index') { - this.notifier.error(err.message) - return - } - - this.notifier.error( - $localize`Search index is unavailable. Retrying with instance results instead.`, - $localize`Search error` - ) - this.advancedSearch.searchTarget = 'local' - this.search() + this.getVideoChannelObs(), + this.getVideoPlaylistObs(), + this.getVideosObs() + ]).subscribe(results => { + for (const result of results) { + this.results = this.results.concat(result.data) } - ) + + this.pagination.totalItems = results.reduce((p, r) => p += r.total, 0) + this.lastSearchTarget = this.advancedSearch.searchTarget + + this.hasMoreResults = this.results.length < this.pagination.totalItems + }, + + err => { + if (this.advancedSearch.searchTarget !== 'search-index') { + this.notifier.error(err.message) + return + } + + this.notifier.error( + $localize`Search index is unavailable. Retrying with instance results instead.`, + $localize`Search error` + ) + this.advancedSearch.searchTarget = 'local' + this.search() + }, + + () => { + this.isSearching = false + }) } onNearOfBottom () { // Last page - if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return + if (!this.hasMoreResults || this.isSearching) return this.pagination.currentPage += 1 this.search() @@ -190,18 +181,33 @@ export class SearchComponent implements OnInit, OnDestroy { return this.advancedSearch.size() } - // Add VideoChannel for typings, but the template already checks "video" argument is a video - removeVideoFromArray (video: Video | VideoChannel) { + // Add VideoChannel/VideoPlaylist for typings, but the template already checks "video" argument is a video + removeVideoFromArray (video: Video | VideoChannel | VideoPlaylist) { this.results = this.results.filter(r => !this.isVideo(r) || r.id !== video.id) } + getLinkType (): LinkType { + if (this.advancedSearch.searchTarget === 'search-index') { + const remoteUriConfig = this.serverConfig.search.remoteUri + + // Redirect on the external instance if not allowed to fetch remote data + if ((!this.isUserLoggedIn() && !remoteUriConfig.anonymous) || !remoteUriConfig.users) { + return 'external' + } + + return 'lazy-load' + } + + return 'internal' + } + isExternalChannelUrl () { - return this.getVideoLinkType() === 'external' + return this.getLinkType() === 'external' } getExternalChannelUrl (channel: VideoChannel) { // Same algorithm than videos - if (this.getVideoLinkType() === 'external') { + if (this.getLinkType() === 'external') { return channel.url } @@ -210,7 +216,7 @@ export class SearchComponent implements OnInit, OnDestroy { } getInternalChannelUrl (channel: VideoChannel) { - const linkType = this.getVideoLinkType() + const linkType = this.getLinkType() if (linkType === 'internal') { return [ '/c', channel.nameWithHost ] @@ -256,7 +262,7 @@ export class SearchComponent implements OnInit, OnDestroy { private getVideosObs () { const params = { search: this.currentSearch, - componentPagination: this.pagination, + componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.videosPerPage }), advancedSearch: this.advancedSearch } @@ -287,6 +293,24 @@ export class SearchComponent implements OnInit, OnDestroy { ) } + private getVideoPlaylistObs () { + if (!this.currentSearch) return of({ data: [], total: 0 }) + + const params = { + search: this.currentSearch, + componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.playlistsPerPage }), + searchTarget: this.advancedSearch.searchTarget + } + + return this.hooks.wrapObsFun( + this.searchService.searchVideoPlaylists.bind(this.searchService), + params, + 'search', + 'filter:api.search.video-playlists.list.params', + 'filter:api.search.video-playlists.list.result' + ) + } + private getDefaultSearchTarget (): SearchTargetType { const searchIndexConfig = this.serverConfig.search.searchIndex diff --git a/client/src/app/+search/search.module.ts b/client/src/app/+search/search.module.ts index 390833abc..26f1523fd 100644 --- a/client/src/app/+search/search.module.ts +++ b/client/src/app/+search/search.module.ts @@ -5,12 +5,12 @@ import { SharedMainModule } from '@app/shared/shared-main' import { SharedSearchModule } from '@app/shared/shared-search' import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' +import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist' import { SearchService } from '../shared/shared-search/search.service' -import { ChannelLazyLoadResolver } from './channel-lazy-load.resolver' import { SearchFiltersComponent } from './search-filters.component' import { SearchRoutingModule } from './search-routing.module' import { SearchComponent } from './search.component' -import { VideoLazyLoadResolver } from './video-lazy-load.resolver' +import { ChannelLazyLoadResolver, PlaylistLazyLoadResolver, VideoLazyLoadResolver } from './shared' @NgModule({ imports: [ @@ -21,7 +21,8 @@ import { VideoLazyLoadResolver } from './video-lazy-load.resolver' SharedFormModule, SharedActorImageModule, SharedUserSubscriptionModule, - SharedVideoMiniatureModule + SharedVideoMiniatureModule, + SharedVideoPlaylistModule ], declarations: [ @@ -36,7 +37,8 @@ import { VideoLazyLoadResolver } from './video-lazy-load.resolver' providers: [ SearchService, VideoLazyLoadResolver, - ChannelLazyLoadResolver + ChannelLazyLoadResolver, + PlaylistLazyLoadResolver ] }) export class SearchModule { } diff --git a/client/src/app/+search/video-lazy-load.resolver.ts b/client/src/app/+search/shared/abstract-lazy-load.resolver.ts similarity index 55% rename from client/src/app/+search/video-lazy-load.resolver.ts rename to client/src/app/+search/shared/abstract-lazy-load.resolver.ts index e43e0089b..31240f451 100644 --- a/client/src/app/+search/video-lazy-load.resolver.ts +++ b/client/src/app/+search/shared/abstract-lazy-load.resolver.ts @@ -1,14 +1,10 @@ +import { Observable } from 'rxjs' import { map } from 'rxjs/operators' -import { Injectable } from '@angular/core' import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router' -import { SearchService } from '@app/shared/shared-search' +import { ResultList } from '@shared/models/result-list.model' -@Injectable() -export class VideoLazyLoadResolver implements Resolve { - constructor ( - private router: Router, - private searchService: SearchService - ) { } +export abstract class AbstractLazyLoadResolver implements Resolve { + protected router: Router resolve (route: ActivatedRouteSnapshot) { const url = route.params.url @@ -18,7 +14,7 @@ export class VideoLazyLoadResolver implements Resolve { return this.router.navigateByUrl('/404') } - return this.searchService.searchVideos({ search: url }) + return this.finder(url) .pipe( map(result => { if (result.data.length !== 1) { @@ -26,10 +22,13 @@ export class VideoLazyLoadResolver implements Resolve { return this.router.navigateByUrl('/404') } - const video = result.data[0] + const redirectUrl = this.buildUrl(result.data[0]) - return this.router.navigateByUrl('/w/' + video.uuid) + return this.router.navigateByUrl(redirectUrl) }) ) } + + protected abstract finder (url: string): Observable> + protected abstract buildUrl (e: T): string } diff --git a/client/src/app/+search/shared/channel-lazy-load.resolver.ts b/client/src/app/+search/shared/channel-lazy-load.resolver.ts new file mode 100644 index 000000000..5e010f795 --- /dev/null +++ b/client/src/app/+search/shared/channel-lazy-load.resolver.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core' +import { Router } from '@angular/router' +import { VideoChannel } from '@app/shared/shared-main' +import { SearchService } from '@app/shared/shared-search' +import { AbstractLazyLoadResolver } from './abstract-lazy-load.resolver' + +@Injectable() +export class ChannelLazyLoadResolver extends AbstractLazyLoadResolver { + + constructor ( + protected router: Router, + private searchService: SearchService + ) { + super() + } + + protected finder (url: string) { + return this.searchService.searchVideoChannels({ search: url }) + } + + protected buildUrl (channel: VideoChannel) { + return '/video-channels/' + channel.nameWithHost + } +} diff --git a/client/src/app/+search/shared/index.ts b/client/src/app/+search/shared/index.ts new file mode 100644 index 000000000..1e68989ae --- /dev/null +++ b/client/src/app/+search/shared/index.ts @@ -0,0 +1,4 @@ +export * from './abstract-lazy-load.resolver' +export * from './channel-lazy-load.resolver' +export * from './playlist-lazy-load.resolver' +export * from './video-lazy-load.resolver' diff --git a/client/src/app/+search/shared/playlist-lazy-load.resolver.ts b/client/src/app/+search/shared/playlist-lazy-load.resolver.ts new file mode 100644 index 000000000..14ae798df --- /dev/null +++ b/client/src/app/+search/shared/playlist-lazy-load.resolver.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core' +import { Router } from '@angular/router' +import { SearchService } from '@app/shared/shared-search' +import { VideoPlaylist } from '@app/shared/shared-video-playlist' +import { AbstractLazyLoadResolver } from './abstract-lazy-load.resolver' + +@Injectable() +export class PlaylistLazyLoadResolver extends AbstractLazyLoadResolver { + + constructor ( + protected router: Router, + private searchService: SearchService + ) { + super() + } + + protected finder (url: string) { + return this.searchService.searchVideoPlaylists({ search: url }) + } + + protected buildUrl (playlist: VideoPlaylist) { + return '/w/p/' + playlist.uuid + } +} diff --git a/client/src/app/+search/shared/video-lazy-load.resolver.ts b/client/src/app/+search/shared/video-lazy-load.resolver.ts new file mode 100644 index 000000000..12b5b2e82 --- /dev/null +++ b/client/src/app/+search/shared/video-lazy-load.resolver.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core' +import { Router } from '@angular/router' +import { Video } from '@app/shared/shared-main' +import { SearchService } from '@app/shared/shared-search' +import { AbstractLazyLoadResolver } from './abstract-lazy-load.resolver' + +@Injectable() +export class VideoLazyLoadResolver extends AbstractLazyLoadResolver