Add ability to display all channel/account videos

This commit is contained in:
Chocobozzz 2020-11-18 15:29:38 +01:00
parent ff2cac9fa3
commit 0aa52e1707
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
17 changed files with 161 additions and 41 deletions

View File

@ -3,7 +3,7 @@ import { concatMap, map, switchMap, tap } from 'rxjs/operators'
import { Component, OnDestroy, OnInit } from '@angular/core' import { Component, OnDestroy, OnInit } from '@angular/core'
import { ComponentPagination, hasMoreItems, ScreenService, User, UserService } from '@app/core' import { ComponentPagination, hasMoreItems, ScreenService, User, UserService } from '@app/core'
import { Account, AccountService, Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' import { Account, AccountService, Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
import { VideoSortField } from '@shared/models' import { NSFWPolicyType, VideoSortField } from '@shared/models'
@Component({ @Component({
selector: 'my-account-video-channels', selector: 'my-account-video-channels',
@ -31,6 +31,7 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
onChannelDataSubject = new Subject<any>() onChannelDataSubject = new Subject<any>()
userMiniature: User userMiniature: User
nsfwPolicy: NSFWPolicyType
private accountSub: Subscription private accountSub: Subscription
@ -52,7 +53,11 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
}) })
this.userService.getAnonymousOrLoggedUser() this.userService.getAnonymousOrLoggedUser()
.subscribe(user => this.userMiniature = user) .subscribe(user => {
this.userMiniature = user
this.nsfwPolicy = user.nsfwPolicy
})
} }
ngOnDestroy () { ngOnDestroy () {
@ -65,7 +70,14 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
tap(res => this.channelPagination.totalItems = res.total), tap(res => this.channelPagination.totalItems = res.total),
switchMap(res => from(res.data)), switchMap(res => from(res.data)),
concatMap(videoChannel => { concatMap(videoChannel => {
return this.videoService.getVideoChannelVideos(videoChannel, this.videosPagination, this.videosSort) const options = {
videoChannel,
videoPagination: this.videosPagination,
sort: this.videosSort,
nsfwPolicy: this.nsfwPolicy
}
return this.videoService.getVideoChannelVideos(options)
.pipe(map(data => ({ videoChannel, videos: data.data }))) .pipe(map(data => ({ videoChannel, videos: data.data })))
}) })
) )

View File

@ -6,6 +6,7 @@ import { AuthService, ConfirmService, LocalStorageService, Notifier, ScreenServi
import { immutableAssign } from '@app/helpers' import { immutableAssign } from '@app/helpers'
import { Account, AccountService, VideoService } from '@app/shared/shared-main' import { Account, AccountService, VideoService } from '@app/shared/shared-main'
import { AbstractVideoList } from '@app/shared/shared-video-miniature' import { AbstractVideoList } from '@app/shared/shared-video-miniature'
import { VideoFilter } from '@shared/models'
@Component({ @Component({
selector: 'my-account-videos', selector: 'my-account-videos',
@ -18,6 +19,8 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
titlePage: string titlePage: string
loadOnInit = false loadOnInit = false
filter: VideoFilter = null
private account: Account private account: Account
private accountSub: Subscription private accountSub: Subscription
@ -40,6 +43,8 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
ngOnInit () { ngOnInit () {
super.ngOnInit() super.ngOnInit()
this.enableAllFilterIfPossible()
// Parent get the account for us // Parent get the account for us
this.accountSub = this.accountService.accountLoaded this.accountSub = this.accountService.accountLoaded
.pipe(first()) .pipe(first())
@ -59,9 +64,16 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
getVideosObservable (page: number) { getVideosObservable (page: number) {
const newPagination = immutableAssign(this.pagination, { currentPage: page }) const newPagination = immutableAssign(this.pagination, { currentPage: page })
const options = {
account: this.account,
videoPagination: newPagination,
sort: this.sort,
nsfwPolicy: this.nsfwPolicy,
videoFilter: this.filter
}
return this.videoService return this.videoService
.getAccountVideos(this.account, newPagination, this.sort) .getAccountVideos(options)
.pipe( .pipe(
tap(({ total }) => { tap(({ total }) => {
this.titlePage = $localize`Published ${total} videos` this.titlePage = $localize`Published ${total} videos`
@ -69,6 +81,12 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
) )
} }
toggleModerationDisplay () {
this.filter = this.buildLocalFilter(this.filter, null)
this.reloadVideos()
}
generateSyndicationList () { generateSyndicationList () {
this.syndicationItems = this.videoService.getAccountFeedUrls(this.account.id) this.syndicationItems = this.videoService.getAccountFeedUrls(this.account.id)
} }

View File

@ -6,6 +6,7 @@ import { AuthService, ConfirmService, LocalStorageService, Notifier, ScreenServi
import { immutableAssign } from '@app/helpers' import { immutableAssign } from '@app/helpers'
import { VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' import { VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
import { AbstractVideoList } from '@app/shared/shared-video-miniature' import { AbstractVideoList } from '@app/shared/shared-video-miniature'
import { VideoFilter } from '@shared/models'
@Component({ @Component({
selector: 'my-video-channel-videos', selector: 'my-video-channel-videos',
@ -18,6 +19,8 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
titlePage: string titlePage: string
loadOnInit = false loadOnInit = false
filter: VideoFilter = null
private videoChannel: VideoChannel private videoChannel: VideoChannel
private videoChannelSub: Subscription private videoChannelSub: Subscription
@ -46,6 +49,8 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
ngOnInit () { ngOnInit () {
super.ngOnInit() super.ngOnInit()
this.enableAllFilterIfPossible()
// Parent get the video channel for us // Parent get the video channel for us
this.videoChannelSub = this.videoChannelService.videoChannelLoaded this.videoChannelSub = this.videoChannelService.videoChannelLoaded
.pipe(first()) .pipe(first())
@ -65,9 +70,16 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
getVideosObservable (page: number) { getVideosObservable (page: number) {
const newPagination = immutableAssign(this.pagination, { currentPage: page }) const newPagination = immutableAssign(this.pagination, { currentPage: page })
const options = {
videoChannel: this.videoChannel,
videoPagination: newPagination,
sort: this.sort,
nsfwPolicy: this.nsfwPolicy,
videoFilter: this.filter
}
return this.videoService return this.videoService
.getVideoChannelVideos(this.videoChannel, newPagination, this.sort, this.nsfwPolicy) .getVideoChannelVideos(options)
.pipe( .pipe(
tap(({ total }) => { tap(({ total }) => {
this.titlePage = total === 1 this.titlePage = total === 1
@ -80,4 +92,10 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
generateSyndicationList () { generateSyndicationList () {
this.syndicationItems = this.videoService.getVideoChannelFeedUrls(this.videoChannel.id) this.syndicationItems = this.videoService.getVideoChannelFeedUrls(this.videoChannel.id)
} }
toggleModerationDisplay () {
this.filter = this.buildLocalFilter(this.filter, null)
this.reloadVideos()
}
} }

View File

@ -39,11 +39,7 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On
ngOnInit () { ngOnInit () {
super.ngOnInit() super.ngOnInit()
if (this.authService.isLoggedIn()) { this.enableAllFilterIfPossible()
const user = this.authService.getUser()
this.displayModerationBlock = user.hasRight(UserRight.SEE_ALL_VIDEOS)
}
this.generateSyndicationList() this.generateSyndicationList()
} }
@ -77,7 +73,7 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On
} }
toggleModerationDisplay () { toggleModerationDisplay () {
this.filter = this.filter === 'local' ? 'all-local' as 'all-local' : 'local' as 'local' this.filter = this.buildLocalFilter(this.filter, 'local')
this.reloadVideos() this.reloadVideos()
} }

View File

@ -134,16 +134,28 @@ export class VideoService implements VideosProvider {
) )
} }
getAccountVideos ( getAccountVideos (parameters: {
account: Account, account: Account,
videoPagination: ComponentPaginationLight, videoPagination: ComponentPaginationLight,
sort: VideoSortField sort: VideoSortField
): Observable<ResultList<Video>> { nsfwPolicy?: NSFWPolicyType
videoFilter?: VideoFilter
}): Observable<ResultList<Video>> {
const { account, videoPagination, sort, videoFilter, nsfwPolicy } = parameters
const pagination = this.restService.componentPaginationToRestPagination(videoPagination) const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
let params = new HttpParams() let params = new HttpParams()
params = this.restService.addRestGetParams(params, pagination, sort) params = this.restService.addRestGetParams(params, pagination, sort)
if (nsfwPolicy) {
params = params.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy))
}
if (videoFilter) {
params = params.set('filter', videoFilter)
}
return this.authHttp return this.authHttp
.get<ResultList<Video>>(AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/videos', { params }) .get<ResultList<Video>>(AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/videos', { params })
.pipe( .pipe(
@ -152,12 +164,15 @@ export class VideoService implements VideosProvider {
) )
} }
getVideoChannelVideos ( getVideoChannelVideos (parameters: {
videoChannel: VideoChannel, videoChannel: VideoChannel,
videoPagination: ComponentPaginationLight, videoPagination: ComponentPaginationLight,
sort: VideoSortField, sort: VideoSortField,
nsfwPolicy?: NSFWPolicyType nsfwPolicy?: NSFWPolicyType
): Observable<ResultList<Video>> { videoFilter?: VideoFilter
}): Observable<ResultList<Video>> {
const { videoChannel, videoPagination, sort, nsfwPolicy, videoFilter } = parameters
const pagination = this.restService.componentPaginationToRestPagination(videoPagination) const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
let params = new HttpParams() let params = new HttpParams()
@ -167,6 +182,10 @@ export class VideoService implements VideosProvider {
params = params.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy)) params = params.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy))
} }
if (videoFilter) {
params = params.set('filter', videoFilter)
}
return this.authHttp return this.authHttp
.get<ResultList<Video>>(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost + '/videos', { params }) .get<ResultList<Video>>(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost + '/videos', { params })
.pipe( .pipe(

View File

@ -21,7 +21,7 @@
<div class="dropdown-item"> <div class="dropdown-item">
<my-peertube-checkbox <my-peertube-checkbox
(change)="toggleModerationDisplay()" (change)="toggleModerationDisplay()"
inputName="display-unlisted-private" i18n-labelText labelText="Display unlisted and private videos" inputName="display-unlisted-private" i18n-labelText labelText="Display all videos (private, unlisted or not yet published)"
></my-peertube-checkbox> ></my-peertube-checkbox>
</div> </div>
</div> </div>

View File

@ -31,13 +31,21 @@ $iconSize: 16px;
.moderation-block { .moderation-block {
div { div {
@include button-with-icon($iconSize, 3px, -1px); @include button-with-icon($iconSize, 3px, -2px);
} }
margin-left: .2rem; margin-left: .4rem;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
.dropdown-item {
padding: 0;
::ng-deep my-peertube-checkbox label {
padding: 3px 15px;
}
}
} }
} }

View File

@ -15,7 +15,7 @@ import {
import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook'
import { GlobalIconName } from '@app/shared/shared-icons' import { GlobalIconName } from '@app/shared/shared-icons'
import { isLastMonth, isLastWeek, isThisMonth, isToday, isYesterday } from '@shared/core-utils/miscs/date' import { isLastMonth, isLastWeek, isThisMonth, isToday, isYesterday } from '@shared/core-utils/miscs/date'
import { ServerConfig, VideoSortField } from '@shared/models' import { ServerConfig, UserRight, VideoFilter, VideoSortField } from '@shared/models'
import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type' import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type'
import { Syndication, Video } from '../shared-main' import { Syndication, Video } from '../shared-main'
import { MiniatureDisplayOptions, OwnerDisplayType } from './video-miniature.component' import { MiniatureDisplayOptions, OwnerDisplayType } from './video-miniature.component'
@ -205,10 +205,6 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
this.loadMoreVideos(true) this.loadMoreVideos(true)
} }
toggleModerationDisplay () {
throw new Error('toggleModerationDisplay is not implemented')
}
removeVideoFromArray (video: Video) { removeVideoFromArray (video: Video) {
this.videos = this.videos.filter(v => v.id !== video.id) this.videos = this.videos.filter(v => v.id !== video.id)
} }
@ -268,6 +264,10 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
return this.groupedDateLabels[this.groupedDates[video.id]] return this.groupedDateLabels[this.groupedDates[video.id]]
} }
toggleModerationDisplay () {
throw new Error('toggleModerationDisplay is not implemented')
}
// On videos hook for children that want to do something // On videos hook for children that want to do something
protected onMoreVideos () { /* empty */ } protected onMoreVideos () { /* empty */ }
@ -277,6 +277,28 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
this.angularState = routeParams[ 'a-state' ] this.angularState = routeParams[ 'a-state' ]
} }
protected buildLocalFilter (existing: VideoFilter, base: VideoFilter) {
if (base === 'local') {
return existing === 'local'
? 'all-local' as 'all-local'
: 'local' as 'local'
}
return existing === 'all'
? null
: 'all'
}
protected enableAllFilterIfPossible () {
if (!this.authService.isLoggedIn()) return
this.authService.userInformationLoaded
.subscribe(() => {
const user = this.authService.getUser()
this.displayModerationBlock = user.hasRight(UserRight.SEE_ALL_VIDEOS)
})
}
private calcPageSizes () { private calcPageSizes () {
if (this.screenService.isInMobileView()) { if (this.screenService.isInMobileView()) {
this.pagination.itemsPerPage = 5 this.pagination.itemsPerPage = 5

View File

@ -65,6 +65,10 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
opacity: .9; opacity: .9;
} }
&:active {
color: pvar(--mainForegroundColor) !important;
}
&::after { &::after {
display: none; display: none;
} }

View File

@ -17,7 +17,7 @@ import * as magnetUtil from 'magnet-uri'
const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS
function isVideoFilterValid (filter: VideoFilter) { function isVideoFilterValid (filter: VideoFilter) {
return filter === 'local' || filter === 'all-local' return filter === 'local' || filter === 'all-local' || filter === 'all'
} }
function isVideoCategoryValid (value: any) { function isVideoCategoryValid (value: any) {

View File

@ -429,7 +429,10 @@ const commonVideosFiltersValidator = [
if (areValidationErrors(req, res)) return if (areValidationErrors(req, res)) return
const user = res.locals.oauth ? res.locals.oauth.token.User : undefined const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) { if (
(req.query.filter === 'all-local' || req.query.filter === 'all') &&
(!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)
) {
res.status(401) res.status(401)
.json({ error: 'You are not allowed to see all local videos.' }) .json({ error: 'You are not allowed to see all local videos.' })

View File

@ -89,7 +89,7 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions)
} }
// Only list public/published videos // Only list public/published videos
if (!options.filter || options.filter !== 'all-local') { if (!options.filter || (options.filter !== 'all-local' && options.filter !== 'all')) {
and.push( and.push(
`("video"."state" = ${VideoState.PUBLISHED} OR ` + `("video"."state" = ${VideoState.PUBLISHED} OR ` +
`("video"."state" = ${VideoState.TO_TRANSCODE} AND "video"."waitTranscoding" IS false))` `("video"."state" = ${VideoState.TO_TRANSCODE} AND "video"."waitTranscoding" IS false))`

View File

@ -1085,7 +1085,7 @@ export class VideoModel extends Model<VideoModel> {
historyOfUser?: MUserId historyOfUser?: MUserId
countVideos?: boolean countVideos?: boolean
}) { }) {
if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { if ((options.filter === 'all-local' || options.filter === 'all') && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
throw new Error('Try to filter all-local but no user has not the see all videos right') throw new Error('Try to filter all-local but no user has not the see all videos right')
} }

View File

@ -78,28 +78,33 @@ describe('Test videos filters', function () {
await testEndpoints(server, server.accessToken, 'local', 200) await testEndpoints(server, server.accessToken, 'local', 200)
}) })
it('Should fail to list all-local with a simple user', async function () { it('Should fail to list all-local/all with a simple user', async function () {
await testEndpoints(server, userAccessToken, 'all-local', 401) await testEndpoints(server, userAccessToken, 'all-local', 401)
await testEndpoints(server, userAccessToken, 'all', 401)
}) })
it('Should succeed to list all-local with a moderator', async function () { it('Should succeed to list all-local/all with a moderator', async function () {
await testEndpoints(server, moderatorAccessToken, 'all-local', 200) await testEndpoints(server, moderatorAccessToken, 'all-local', 200)
await testEndpoints(server, moderatorAccessToken, 'all', 200)
}) })
it('Should succeed to list all-local with an admin', async function () { it('Should succeed to list all-local/all with an admin', async function () {
await testEndpoints(server, server.accessToken, 'all-local', 200) await testEndpoints(server, server.accessToken, 'all-local', 200)
await testEndpoints(server, server.accessToken, 'all', 200)
}) })
// Because we cannot authenticate the user on the RSS endpoint // Because we cannot authenticate the user on the RSS endpoint
it('Should fail on the feeds endpoint with the all-local filter', async function () { it('Should fail on the feeds endpoint with the all-local/all filter', async function () {
await makeGetRequest({ for (const filter of [ 'all', 'all-local' ]) {
url: server.url, await makeGetRequest({
path: '/feeds/videos.json', url: server.url,
statusCodeExpected: 401, path: '/feeds/videos.json',
query: { statusCodeExpected: 401,
filter: 'all-local' query: {
} filter
}) }
})
}
}) })
it('Should succeed on the feeds endpoint with the local filter', async function () { it('Should succeed on the feeds endpoint with the local filter', async function () {

View File

@ -116,6 +116,20 @@ describe('Test videos filter validator', function () {
} }
} }
}) })
it('Should display all videos by the admin or the moderator', async function () {
for (const server of servers) {
for (const token of [ server.accessToken, server['moderatorAccessToken'] ]) {
const [ channelVideos, accountVideos, videos, searchVideos ] = await getVideosNames(server, token, 'all')
expect(channelVideos).to.have.lengthOf(3)
expect(accountVideos).to.have.lengthOf(3)
expect(videos).to.have.lengthOf(5)
expect(searchVideos).to.have.lengthOf(5)
}
}
})
}) })
after(async function () { after(async function () {

View File

@ -1 +1 @@
export type VideoFilter = 'local' | 'all-local' export type VideoFilter = 'local' | 'all-local' | 'all'

View File

@ -3681,9 +3681,10 @@ components:
in: query in: query
required: false required: false
description: > description: >
Special filters (local for instance) which might require special rights: Special filters which might require special rights:
* `local` - only videos local to the instance * `local` - only videos local to the instance
* `all-local` - only videos local to the instance, but showing private and unlisted videos (requires Admin privileges) * `all-local` - only videos local to the instance, but showing private and unlisted videos (requires Admin privileges)
* `all` - all videos, showing private and unlisted videos (requires Admin privileges)
schema: schema:
type: string type: string
enum: enum: