Browse Source

Add ability to display all channel/account videos

pull/3334/head
Chocobozzz 1 week ago
parent
commit
0aa52e1707
No known key found for this signature in database GPG Key ID: 583A612D890159BE
17 changed files with 161 additions and 41 deletions
  1. +15
    -3
      client/src/app/+accounts/account-video-channels/account-video-channels.component.ts
  2. +19
    -1
      client/src/app/+accounts/account-videos/account-videos.component.ts
  3. +19
    -1
      client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
  4. +2
    -6
      client/src/app/+videos/video-list/video-local.component.ts
  5. +23
    -4
      client/src/app/shared/shared-main/video/video.service.ts
  6. +1
    -1
      client/src/app/shared/shared-video-miniature/abstract-video-list.html
  7. +10
    -2
      client/src/app/shared/shared-video-miniature/abstract-video-list.scss
  8. +27
    -5
      client/src/app/shared/shared-video-miniature/abstract-video-list.ts
  9. +4
    -0
      client/src/sass/bootstrap.scss
  10. +1
    -1
      server/helpers/custom-validators/videos.ts
  11. +4
    -1
      server/middlewares/validators/videos/videos.ts
  12. +1
    -1
      server/models/video/video-query-builder.ts
  13. +1
    -1
      server/models/video/video.ts
  14. +17
    -12
      server/tests/api/check-params/videos-filter.ts
  15. +14
    -0
      server/tests/api/videos/videos-filter.ts
  16. +1
    -1
      shared/models/videos/video-query.type.ts
  17. +2
    -1
      support/doc/api/openapi.yaml

+ 15
- 3
client/src/app/+accounts/account-video-channels/account-video-channels.component.ts View File

@@ -3,7 +3,7 @@ import { concatMap, map, switchMap, tap } from 'rxjs/operators'
import { Component, OnDestroy, OnInit } from '@angular/core'
import { ComponentPagination, hasMoreItems, ScreenService, User, UserService } from '@app/core'
import { Account, AccountService, Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
import { VideoSortField } from '@shared/models'
import { NSFWPolicyType, VideoSortField } from '@shared/models'

@Component({
selector: 'my-account-video-channels',
@@ -31,6 +31,7 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
onChannelDataSubject = new Subject<any>()

userMiniature: User
nsfwPolicy: NSFWPolicyType

private accountSub: Subscription

@@ -52,7 +53,11 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
})

this.userService.getAnonymousOrLoggedUser()
.subscribe(user => this.userMiniature = user)
.subscribe(user => {
this.userMiniature = user

this.nsfwPolicy = user.nsfwPolicy
})
}

ngOnDestroy () {
@@ -65,7 +70,14 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
tap(res => this.channelPagination.totalItems = res.total),
switchMap(res => from(res.data)),
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 })))
})
)


+ 19
- 1
client/src/app/+accounts/account-videos/account-videos.component.ts View File

@@ -6,6 +6,7 @@ import { AuthService, ConfirmService, LocalStorageService, Notifier, ScreenServi
import { immutableAssign } from '@app/helpers'
import { Account, AccountService, VideoService } from '@app/shared/shared-main'
import { AbstractVideoList } from '@app/shared/shared-video-miniature'
import { VideoFilter } from '@shared/models'

@Component({
selector: 'my-account-videos',
@@ -18,6 +19,8 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
titlePage: string
loadOnInit = false

filter: VideoFilter = null

private account: Account
private accountSub: Subscription

@@ -40,6 +43,8 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
ngOnInit () {
super.ngOnInit()

this.enableAllFilterIfPossible()

// Parent get the account for us
this.accountSub = this.accountService.accountLoaded
.pipe(first())
@@ -59,9 +64,16 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,

getVideosObservable (page: number) {
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
.getAccountVideos(this.account, newPagination, this.sort)
.getAccountVideos(options)
.pipe(
tap(({ total }) => {
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 () {
this.syndicationItems = this.videoService.getAccountFeedUrls(this.account.id)
}


+ 19
- 1
client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts View File

@@ -6,6 +6,7 @@ import { AuthService, ConfirmService, LocalStorageService, Notifier, ScreenServi
import { immutableAssign } from '@app/helpers'
import { VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
import { AbstractVideoList } from '@app/shared/shared-video-miniature'
import { VideoFilter } from '@shared/models'

@Component({
selector: 'my-video-channel-videos',
@@ -18,6 +19,8 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
titlePage: string
loadOnInit = false

filter: VideoFilter = null

private videoChannel: VideoChannel
private videoChannelSub: Subscription

@@ -46,6 +49,8 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
ngOnInit () {
super.ngOnInit()

this.enableAllFilterIfPossible()

// Parent get the video channel for us
this.videoChannelSub = this.videoChannelService.videoChannelLoaded
.pipe(first())
@@ -65,9 +70,16 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On

getVideosObservable (page: number) {
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
.getVideoChannelVideos(this.videoChannel, newPagination, this.sort, this.nsfwPolicy)
.getVideoChannelVideos(options)
.pipe(
tap(({ total }) => {
this.titlePage = total === 1
@@ -80,4 +92,10 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
generateSyndicationList () {
this.syndicationItems = this.videoService.getVideoChannelFeedUrls(this.videoChannel.id)
}

toggleModerationDisplay () {
this.filter = this.buildLocalFilter(this.filter, null)

this.reloadVideos()
}
}

+ 2
- 6
client/src/app/+videos/video-list/video-local.component.ts View File

@@ -39,11 +39,7 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On
ngOnInit () {
super.ngOnInit()

if (this.authService.isLoggedIn()) {
const user = this.authService.getUser()
this.displayModerationBlock = user.hasRight(UserRight.SEE_ALL_VIDEOS)
}

this.enableAllFilterIfPossible()
this.generateSyndicationList()
}

@@ -77,7 +73,7 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On
}

toggleModerationDisplay () {
this.filter = this.filter === 'local' ? 'all-local' as 'all-local' : 'local' as 'local'
this.filter = this.buildLocalFilter(this.filter, 'local')

this.reloadVideos()
}


+ 23
- 4
client/src/app/shared/shared-main/video/video.service.ts View File

@@ -134,16 +134,28 @@ export class VideoService implements VideosProvider {
)
}

getAccountVideos (
getAccountVideos (parameters: {
account: Account,
videoPagination: ComponentPaginationLight,
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)

let params = new HttpParams()
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
.get<ResultList<Video>>(AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/videos', { params })
.pipe(
@@ -152,12 +164,15 @@ export class VideoService implements VideosProvider {
)
}

getVideoChannelVideos (
getVideoChannelVideos (parameters: {
videoChannel: VideoChannel,
videoPagination: ComponentPaginationLight,
sort: VideoSortField,
nsfwPolicy?: NSFWPolicyType
): Observable<ResultList<Video>> {
videoFilter?: VideoFilter
}): Observable<ResultList<Video>> {
const { videoChannel, videoPagination, sort, nsfwPolicy, videoFilter } = parameters

const pagination = this.restService.componentPaginationToRestPagination(videoPagination)

let params = new HttpParams()
@@ -167,6 +182,10 @@ export class VideoService implements VideosProvider {
params = params.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy))
}

if (videoFilter) {
params = params.set('filter', videoFilter)
}

return this.authHttp
.get<ResultList<Video>>(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost + '/videos', { params })
.pipe(


+ 1
- 1
client/src/app/shared/shared-video-miniature/abstract-video-list.html View File

@@ -21,7 +21,7 @@
<div class="dropdown-item">
<my-peertube-checkbox
(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>
</div>
</div>


+ 10
- 2
client/src/app/shared/shared-video-miniature/abstract-video-list.scss View File

@@ -31,13 +31,21 @@ $iconSize: 16px;

.moderation-block {
div {
@include button-with-icon($iconSize, 3px, -1px);
@include button-with-icon($iconSize, 3px, -2px);
}

margin-left: .2rem;
margin-left: .4rem;
display: flex;
justify-content: flex-end;
align-items: center;

.dropdown-item {
padding: 0;

::ng-deep my-peertube-checkbox label {
padding: 3px 15px;
}
}
}
}



+ 27
- 5
client/src/app/shared/shared-video-miniature/abstract-video-list.ts View File

@@ -15,7 +15,7 @@ import {
import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook'
import { GlobalIconName } from '@app/shared/shared-icons'
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 { Syndication, Video } from '../shared-main'
import { MiniatureDisplayOptions, OwnerDisplayType } from './video-miniature.component'
@@ -205,10 +205,6 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
this.loadMoreVideos(true)
}

toggleModerationDisplay () {
throw new Error('toggleModerationDisplay is not implemented')
}

removeVideoFromArray (video: Video) {
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]]
}

toggleModerationDisplay () {
throw new Error('toggleModerationDisplay is not implemented')
}

// On videos hook for children that want to do something
protected onMoreVideos () { /* empty */ }

@@ -277,6 +277,28 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
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 () {
if (this.screenService.isInMobileView()) {
this.pagination.itemsPerPage = 5


+ 4
- 0
client/src/sass/bootstrap.scss View File

@@ -65,6 +65,10 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
opacity: .9;
}

&:active {
color: pvar(--mainForegroundColor) !important;
}

&::after {
display: none;
}


+ 1
- 1
server/helpers/custom-validators/videos.ts View File

@@ -17,7 +17,7 @@ import * as magnetUtil from 'magnet-uri'
const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS

function isVideoFilterValid (filter: VideoFilter) {
return filter === 'local' || filter === 'all-local'
return filter === 'local' || filter === 'all-local' || filter === 'all'
}

function isVideoCategoryValid (value: any) {


+ 4
- 1
server/middlewares/validators/videos/videos.ts View File

@@ -429,7 +429,10 @@ const commonVideosFiltersValidator = [
if (areValidationErrors(req, res)) return

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)
.json({ error: 'You are not allowed to see all local videos.' })



+ 1
- 1
server/models/video/video-query-builder.ts View File

@@ -89,7 +89,7 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions)
}

// Only list public/published videos
if (!options.filter || options.filter !== 'all-local') {
if (!options.filter || (options.filter !== 'all-local' && options.filter !== 'all')) {
and.push(
`("video"."state" = ${VideoState.PUBLISHED} OR ` +
`("video"."state" = ${VideoState.TO_TRANSCODE} AND "video"."waitTranscoding" IS false))`


+ 1
- 1
server/models/video/video.ts View File

@@ -1085,7 +1085,7 @@ export class VideoModel extends Model<VideoModel> {
historyOfUser?: MUserId
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')
}



+ 17
- 12
server/tests/api/check-params/videos-filter.ts View File

@@ -78,28 +78,33 @@ describe('Test videos filters', function () {
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', 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', 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', 200)
})

// Because we cannot authenticate the user on the RSS endpoint
it('Should fail on the feeds endpoint with the all-local filter', async function () {
await makeGetRequest({
url: server.url,
path: '/feeds/videos.json',
statusCodeExpected: 401,
query: {
filter: 'all-local'
}
})
it('Should fail on the feeds endpoint with the all-local/all filter', async function () {
for (const filter of [ 'all', 'all-local' ]) {
await makeGetRequest({
url: server.url,
path: '/feeds/videos.json',
statusCodeExpected: 401,
query: {
filter
}
})
}
})

it('Should succeed on the feeds endpoint with the local filter', async function () {


+ 14
- 0
server/tests/api/videos/videos-filter.ts 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 () {


+ 1
- 1
shared/models/videos/video-query.type.ts View File

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

+ 2
- 1
support/doc/api/openapi.yaml View File

@@ -3681,9 +3681,10 @@ components:
in: query
required: false
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
* `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:
type: string
enum:


Loading…
Cancel
Save