mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-10-03 17:59:37 +02:00
feat(config): add admin options to customize default "Browse videos" behaviour (#7193)
* feat(customBrowseVideosDefaultSort): update server config * feat(customBrowseVideosDefaultSort): update client admin-config-general component * feat(customBrowseVideosDefaultSort): add new consistency check to server checker-after-init * feat(customBrowseVideosDefaultSort): update config .yaml with more details about available options * feat(customBrowseVideosDefaultSort): refactor consistency check in server checker-after-init * feat(customBrowseVideosDefaultSort): client, add new select-videos-sort shared component * feat(customBrowseVideosDefaultSort): client, refactor admin-config-general component to use new select-videos-sort shared component * feat(customBrowseVideosDefaultSort): client, fix my-select-videos-sort width in admin-config-general * feat(customBrowseVideosDefaultSort): client, update video-filters-header scss * feat(customBrowseVideosDefaultSort): client, update videos-list-all component * feat(config): refactor checkBrowseVideosConfig logic into separate custom validator * feat(config): refactor isBrowseVideosDefaultSortValid to use template literals * feat(config): refactor isBrowseVideosDefaultSortValid * feat(config): add check for invalid browse videos config to customConfigUpdateValidator * feat(config): group browse-videos tests in describe block * feat(config): refactor to use client.browse_videos section, instead of browse.videos section * feat(config): add browse_videos default_scope config key (config and server changes) * feat(config): add browse_videos default_scope config key (client changes) * Reorder browse videos before videos * Fix i18n message --------- Co-authored-by: Chocobozzz <me@florianbigard.com>
This commit is contained in:
parent
9b7edd1c59
commit
e74bf8ae2a
23 changed files with 436 additions and 54 deletions
|
@ -36,6 +36,8 @@ input[type="checkbox"] {
|
||||||
@include peertube-select-container($form-base-input-width);
|
@include peertube-select-container($form-base-input-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
my-select-videos-sort,
|
||||||
|
my-select-videos-scope,
|
||||||
my-select-checkbox,
|
my-select-checkbox,
|
||||||
my-select-options,
|
my-select-options,
|
||||||
my-select-custom-value {
|
my-select-custom-value {
|
||||||
|
|
|
@ -320,6 +320,34 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-two-cols mt-4">
|
||||||
|
<div class="title-col">
|
||||||
|
<h2 i18n>BROWSE VIDEOS</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-col">
|
||||||
|
<div class="form-group" formGroupName="client">
|
||||||
|
<ng-container formGroupName="browseVideos">
|
||||||
|
<div class="form-group">
|
||||||
|
<label i18n for="browseVideosDefaultSort">Default sort</label>
|
||||||
|
|
||||||
|
<my-select-videos-sort inputId="browseVideosDefaultSort" formControlName="defaultSort"></my-select-videos-sort>
|
||||||
|
|
||||||
|
<div *ngIf="formErrors.client.browseVideos.defaultSort" class="form-error" role="alert">{{ formErrors.client.browseVideos.defaultSort }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label i18n for="browseVideosDefaultScope">Default scope</label>
|
||||||
|
|
||||||
|
<my-select-videos-scope inputId="browseVideosDefaultScope" formControlName="defaultScope"></my-select-videos-scope>
|
||||||
|
|
||||||
|
<div *ngIf="formErrors.client.browseVideos.defaultScope" class="form-error" role="alert">{{ formErrors.client.browseVideos.defaultScope }}</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="pt-two-cols mt-4">
|
<div class="pt-two-cols mt-4">
|
||||||
<div class="title-col">
|
<div class="title-col">
|
||||||
<h2 i18n>VIDEOS</h2>
|
<h2 i18n>VIDEOS</h2>
|
||||||
|
|
|
@ -33,6 +33,8 @@ import { MarkdownTextareaComponent } from '../../../shared/shared-forms/markdown
|
||||||
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
|
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
|
||||||
import { SelectCustomValueComponent } from '../../../shared/shared-forms/select/select-custom-value.component'
|
import { SelectCustomValueComponent } from '../../../shared/shared-forms/select/select-custom-value.component'
|
||||||
import { SelectOptionsComponent } from '../../../shared/shared-forms/select/select-options.component'
|
import { SelectOptionsComponent } from '../../../shared/shared-forms/select/select-options.component'
|
||||||
|
import { SelectVideosSortComponent } from '../../../shared/shared-forms/select/select-videos-sort.component'
|
||||||
|
import { SelectVideosScopeComponent } from '../../../shared/shared-forms/select/select-videos-scope.component'
|
||||||
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
||||||
import { UserRealQuotaInfoComponent } from '../../shared/user-real-quota-info.component'
|
import { UserRealQuotaInfoComponent } from '../../shared/user-real-quota-info.component'
|
||||||
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
import { AdminSaveBarComponent } from '../shared/admin-save-bar.component'
|
||||||
|
@ -43,6 +45,11 @@ type Form = {
|
||||||
}>
|
}>
|
||||||
|
|
||||||
client: FormGroup<{
|
client: FormGroup<{
|
||||||
|
browseVideos: FormGroup<{
|
||||||
|
defaultSort: FormControl<string>
|
||||||
|
defaultScope: FormControl<string>
|
||||||
|
}>
|
||||||
|
|
||||||
menu: FormGroup<{
|
menu: FormGroup<{
|
||||||
login: FormGroup<{
|
login: FormGroup<{
|
||||||
redirectOnSingleExternalAuth: FormControl<boolean>
|
redirectOnSingleExternalAuth: FormControl<boolean>
|
||||||
|
@ -220,7 +227,9 @@ type Form = {
|
||||||
UserRealQuotaInfoComponent,
|
UserRealQuotaInfoComponent,
|
||||||
SelectOptionsComponent,
|
SelectOptionsComponent,
|
||||||
AlertComponent,
|
AlertComponent,
|
||||||
AdminSaveBarComponent
|
AdminSaveBarComponent,
|
||||||
|
SelectVideosSortComponent,
|
||||||
|
SelectVideosScopeComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AdminConfigGeneralComponent implements OnInit, OnDestroy, CanComponentDeactivate {
|
export class AdminConfigGeneralComponent implements OnInit, OnDestroy, CanComponentDeactivate {
|
||||||
|
@ -295,6 +304,10 @@ export class AdminConfigGeneralComponent implements OnInit, OnDestroy, CanCompon
|
||||||
defaultClientRoute: null
|
defaultClientRoute: null
|
||||||
},
|
},
|
||||||
client: {
|
client: {
|
||||||
|
browseVideos: {
|
||||||
|
defaultSort: null,
|
||||||
|
defaultScope: null
|
||||||
|
},
|
||||||
menu: {
|
menu: {
|
||||||
login: {
|
login: {
|
||||||
redirectOnSingleExternalAuth: null
|
redirectOnSingleExternalAuth: null
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
|
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
|
||||||
import { ActivatedRoute } from '@angular/router'
|
import { ActivatedRoute } from '@angular/router'
|
||||||
import { ComponentPaginationLight, DisableForReuseHook, MetaService } from '@app/core'
|
import { ComponentPaginationLight, DisableForReuseHook, MetaService, ServerService } from '@app/core'
|
||||||
import { HooksService } from '@app/core/plugins/hooks.service'
|
import { HooksService } from '@app/core/plugins/hooks.service'
|
||||||
import { VideoService } from '@app/shared/shared-main/video/video.service'
|
import { VideoService } from '@app/shared/shared-main/video/video.service'
|
||||||
import { VideoFilterScope, VideoFilters } from '@app/shared/shared-video-miniature/video-filters.model'
|
import { VideoFilterScope, VideoFilters } from '@app/shared/shared-video-miniature/video-filters.model'
|
||||||
import { VideosListComponent } from '@app/shared/shared-video-miniature/videos-list.component'
|
import { VideosListComponent } from '@app/shared/shared-video-miniature/videos-list.component'
|
||||||
import { VideoSortField } from '@peertube/peertube-models'
|
import { HTMLServerConfig, VideoSortField } from '@peertube/peertube-models'
|
||||||
import { Subscription } from 'rxjs'
|
import { Subscription } from 'rxjs'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -19,6 +19,7 @@ export class VideosListAllComponent implements OnInit, OnDestroy, DisableForReus
|
||||||
private videoService = inject(VideoService)
|
private videoService = inject(VideoService)
|
||||||
private hooks = inject(HooksService)
|
private hooks = inject(HooksService)
|
||||||
private meta = inject(MetaService)
|
private meta = inject(MetaService)
|
||||||
|
private serverService = inject(ServerService)
|
||||||
|
|
||||||
getVideosObservableFunction = this.getVideosObservable.bind(this)
|
getVideosObservableFunction = this.getVideosObservable.bind(this)
|
||||||
getSyndicationItemsFunction = this.getSyndicationItems.bind(this)
|
getSyndicationItemsFunction = this.getSyndicationItems.bind(this)
|
||||||
|
@ -37,11 +38,14 @@ export class VideosListAllComponent implements OnInit, OnDestroy, DisableForReus
|
||||||
disabled = false
|
disabled = false
|
||||||
|
|
||||||
private routeSub: Subscription
|
private routeSub: Subscription
|
||||||
|
private serverConfig: HTMLServerConfig
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
|
this.serverConfig = this.serverService.getHTMLConfig()
|
||||||
|
|
||||||
const queryParams = this.route.snapshot.queryParams
|
const queryParams = this.route.snapshot.queryParams
|
||||||
this.defaultSort = queryParams.sort || '-publishedAt'
|
this.defaultSort = queryParams.sort || this.serverConfig.client.browseVideos.defaultSort
|
||||||
this.defaultScope = queryParams.scope || 'federated'
|
this.defaultScope = queryParams.scope || this.serverConfig.client.browseVideos.defaultScope
|
||||||
|
|
||||||
this.routeSub = this.route.params.subscribe(() => this.update())
|
this.routeSub = this.route.params.subscribe(() => this.update())
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { Component, forwardRef, OnInit, input } from '@angular/core'
|
||||||
|
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||||
|
import { SelectOptionsItem } from '../../../../types/select-options-item.model'
|
||||||
|
import { SelectOptionsComponent } from './select-options.component'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-select-videos-scope',
|
||||||
|
template: `
|
||||||
|
<my-select-options
|
||||||
|
[inputId]="inputId()"
|
||||||
|
|
||||||
|
[items]="scopeItems"
|
||||||
|
|
||||||
|
[(ngModel)]="selectedId"
|
||||||
|
(ngModelChange)="onModelChange()"
|
||||||
|
></my-select-options>
|
||||||
|
`,
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: NG_VALUE_ACCESSOR,
|
||||||
|
useExisting: forwardRef(() => SelectVideosScopeComponent),
|
||||||
|
multi: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
imports: [ FormsModule, CommonModule, SelectOptionsComponent ]
|
||||||
|
})
|
||||||
|
export class SelectVideosScopeComponent implements ControlValueAccessor, OnInit {
|
||||||
|
readonly inputId = input.required<string>()
|
||||||
|
|
||||||
|
scopeItems: SelectOptionsItem[]
|
||||||
|
selectedId: string
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.buildScopeItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildScopeItems () {
|
||||||
|
this.scopeItems = [
|
||||||
|
{ id: 'local', label: $localize`Only videos from this platform` },
|
||||||
|
{ id: 'federated', label: $localize`Videos from all platforms` }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
propagateChange = (_: any) => {
|
||||||
|
// empty
|
||||||
|
}
|
||||||
|
|
||||||
|
writeValue (id: string) {
|
||||||
|
this.selectedId = id
|
||||||
|
}
|
||||||
|
|
||||||
|
registerOnChange (fn: (_: any) => void) {
|
||||||
|
this.propagateChange = fn
|
||||||
|
}
|
||||||
|
|
||||||
|
registerOnTouched () {
|
||||||
|
// Unused
|
||||||
|
}
|
||||||
|
|
||||||
|
onModelChange () {
|
||||||
|
this.propagateChange(this.selectedId)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { CommonModule } from '@angular/common'
|
||||||
|
import { Component, forwardRef, OnInit, inject, input } from '@angular/core'
|
||||||
|
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||||
|
import { ServerService } from '@app/core'
|
||||||
|
import { SelectOptionsItem } from '../../../../types/select-options-item.model'
|
||||||
|
import { SelectOptionsComponent } from './select-options.component'
|
||||||
|
import { HTMLServerConfig } from '@peertube/peertube-models'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-select-videos-sort',
|
||||||
|
template: `
|
||||||
|
<my-select-options
|
||||||
|
[inputId]="inputId()"
|
||||||
|
|
||||||
|
[items]="sortItems"
|
||||||
|
|
||||||
|
[(ngModel)]="selectedId"
|
||||||
|
(ngModelChange)="onModelChange()"
|
||||||
|
></my-select-options>
|
||||||
|
`,
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: NG_VALUE_ACCESSOR,
|
||||||
|
useExisting: forwardRef(() => SelectVideosSortComponent),
|
||||||
|
multi: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
imports: [ FormsModule, CommonModule, SelectOptionsComponent ]
|
||||||
|
})
|
||||||
|
export class SelectVideosSortComponent implements ControlValueAccessor, OnInit {
|
||||||
|
private server = inject(ServerService)
|
||||||
|
|
||||||
|
readonly inputId = input.required<string>()
|
||||||
|
|
||||||
|
sortItems: SelectOptionsItem[]
|
||||||
|
selectedId: string
|
||||||
|
|
||||||
|
private serverConfig: HTMLServerConfig
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.serverConfig = this.server.getHTMLConfig()
|
||||||
|
|
||||||
|
this.buildSortItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildSortItems () {
|
||||||
|
this.sortItems = [
|
||||||
|
{ id: '-publishedAt', label: $localize`Recently Added` },
|
||||||
|
{ id: '-originallyPublishedAt', label: $localize`Original Publication Date` },
|
||||||
|
{ id: 'name', label: $localize`Name` }
|
||||||
|
]
|
||||||
|
|
||||||
|
if (this.isTrendingSortEnabled('most-viewed')) {
|
||||||
|
this.sortItems.push({ id: '-trending', label: $localize`Recent Views` })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isTrendingSortEnabled('hot')) {
|
||||||
|
this.sortItems.push({ id: '-hot', label: $localize`Hot` })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isTrendingSortEnabled('most-liked')) {
|
||||||
|
this.sortItems.push({ id: '-likes', label: $localize`Likes` })
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sortItems.push({ id: '-views', label: $localize`Global Views` })
|
||||||
|
}
|
||||||
|
|
||||||
|
private isTrendingSortEnabled (sort: 'most-viewed' | 'hot' | 'most-liked') {
|
||||||
|
return this.serverConfig.trending.videos.algorithms.enabled.includes(sort)
|
||||||
|
}
|
||||||
|
|
||||||
|
propagateChange = (_: any) => {
|
||||||
|
// empty
|
||||||
|
}
|
||||||
|
|
||||||
|
writeValue (id: string) {
|
||||||
|
this.selectedId = id
|
||||||
|
}
|
||||||
|
|
||||||
|
registerOnChange (fn: (_: any) => void) {
|
||||||
|
this.propagateChange = fn
|
||||||
|
}
|
||||||
|
|
||||||
|
registerOnTouched () {
|
||||||
|
// Unused
|
||||||
|
}
|
||||||
|
|
||||||
|
onModelChange () {
|
||||||
|
this.propagateChange(this.selectedId)
|
||||||
|
}
|
||||||
|
}
|
|
@ -59,7 +59,7 @@
|
||||||
<div class="d-flex flex-wrap align-items-center mb-2">
|
<div class="d-flex flex-wrap align-items-center mb-2">
|
||||||
<label for="sort-videos" i18n class="select-label">Sort by:</label>
|
<label for="sort-videos" i18n class="select-label">Sort by:</label>
|
||||||
|
|
||||||
<my-select-options inputId="sort-videos" class="d-inline-block me-2" formControlName="sort" [items]="sortItems"></my-select-options>
|
<my-select-videos-sort inputId="sort-videos" class="d-inline-block me-2" formControlName="sort"></my-select-videos-sort>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="scope" i18n>Displayed videos</label>
|
<label for="scope" i18n>Displayed videos</label>
|
||||||
|
|
||||||
<my-select-options inputId="scope" class="scope-select" formControlName="scope" [items]="availableScopes"></my-select-options>
|
<my-select-videos-scope inputId="scope" class="scope-select" formControlName="scope"></my-select-videos-scope>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -121,9 +121,10 @@ $filters-background: pvar(--bg-secondary-400);
|
||||||
height: min-content;
|
height: min-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
my-select-videos-sort,
|
||||||
|
my-select-videos-scope,
|
||||||
my-select-languages,
|
my-select-languages,
|
||||||
my-select-categories,
|
my-select-categories {
|
||||||
my-select-options {
|
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,11 +8,11 @@ import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { UserRight, VideoConstant } from '@peertube/peertube-models'
|
import { UserRight, VideoConstant } from '@peertube/peertube-models'
|
||||||
import { AttributesOnly } from '@peertube/peertube-typescript-utils'
|
import { AttributesOnly } from '@peertube/peertube-typescript-utils'
|
||||||
import debug from 'debug'
|
import debug from 'debug'
|
||||||
import { SelectOptionsItem } from 'src/types'
|
|
||||||
import { PeertubeCheckboxComponent } from '../shared-forms/peertube-checkbox.component'
|
import { PeertubeCheckboxComponent } from '../shared-forms/peertube-checkbox.component'
|
||||||
import { SelectCategoriesComponent } from '../shared-forms/select/select-categories.component'
|
import { SelectCategoriesComponent } from '../shared-forms/select/select-categories.component'
|
||||||
import { SelectLanguagesComponent } from '../shared-forms/select/select-languages.component'
|
import { SelectLanguagesComponent } from '../shared-forms/select/select-languages.component'
|
||||||
import { SelectOptionsComponent } from '../shared-forms/select/select-options.component'
|
import { SelectVideosSortComponent } from '../shared-forms/select/select-videos-sort.component'
|
||||||
|
import { SelectVideosScopeComponent } from '../shared-forms/select/select-videos-scope.component'
|
||||||
import { GlobalIconComponent, GlobalIconName } from '../shared-icons/global-icon.component'
|
import { GlobalIconComponent, GlobalIconName } from '../shared-icons/global-icon.component'
|
||||||
import { InstanceFollowService } from '../shared-instance/instance-follow.service'
|
import { InstanceFollowService } from '../shared-instance/instance-follow.service'
|
||||||
import { ButtonComponent } from '../shared-main/buttons/button.component'
|
import { ButtonComponent } from '../shared-main/buttons/button.component'
|
||||||
|
@ -43,8 +43,9 @@ type QuickFilter = {
|
||||||
SelectLanguagesComponent,
|
SelectLanguagesComponent,
|
||||||
SelectCategoriesComponent,
|
SelectCategoriesComponent,
|
||||||
PeertubeCheckboxComponent,
|
PeertubeCheckboxComponent,
|
||||||
SelectOptionsComponent,
|
ButtonComponent,
|
||||||
ButtonComponent
|
SelectVideosSortComponent,
|
||||||
|
SelectVideosScopeComponent
|
||||||
],
|
],
|
||||||
providers: [ InstanceFollowService ]
|
providers: [ InstanceFollowService ]
|
||||||
})
|
})
|
||||||
|
@ -65,9 +66,6 @@ export class VideoFiltersHeaderComponent implements OnInit {
|
||||||
|
|
||||||
form: FormGroup
|
form: FormGroup
|
||||||
|
|
||||||
sortItems: SelectOptionsItem[] = []
|
|
||||||
availableScopes: SelectOptionsItem[] = []
|
|
||||||
|
|
||||||
quickFilters: QuickFilter[] = []
|
quickFilters: QuickFilter[] = []
|
||||||
|
|
||||||
instanceName: string
|
instanceName: string
|
||||||
|
@ -109,12 +107,6 @@ export class VideoFiltersHeaderComponent implements OnInit {
|
||||||
this.followService.getFollowing({ pagination: { count: 1, start: 0 }, state: 'accepted' })
|
this.followService.getFollowing({ pagination: { count: 1, start: 0 }, state: 'accepted' })
|
||||||
.subscribe(({ total }) => this.totalFollowing = total)
|
.subscribe(({ total }) => this.totalFollowing = total)
|
||||||
|
|
||||||
this.availableScopes = [
|
|
||||||
{ id: 'local', label: $localize`Only videos from this platform` },
|
|
||||||
{ id: 'federated', label: $localize`Videos from all platforms` }
|
|
||||||
]
|
|
||||||
|
|
||||||
this.buildSortItems()
|
|
||||||
this.buildQuickFilters()
|
this.buildQuickFilters()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,34 +141,6 @@ export class VideoFiltersHeaderComponent implements OnInit {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
private buildSortItems () {
|
|
||||||
this.sortItems = [
|
|
||||||
{ id: '-publishedAt', label: $localize`Recently Added` },
|
|
||||||
{ id: '-originallyPublishedAt', label: $localize`Original Publication Date` },
|
|
||||||
{ id: 'name', label: $localize`Name` }
|
|
||||||
]
|
|
||||||
|
|
||||||
if (this.isTrendingSortEnabled('most-viewed')) {
|
|
||||||
this.sortItems.push({ id: '-trending', label: $localize`Recent Views` })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isTrendingSortEnabled('hot')) {
|
|
||||||
this.sortItems.push({ id: '-hot', label: $localize`Hot` })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isTrendingSortEnabled('most-liked')) {
|
|
||||||
this.sortItems.push({ id: '-likes', label: $localize`Likes` })
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sortItems.push({ id: '-views', label: $localize`Global Views` })
|
|
||||||
}
|
|
||||||
|
|
||||||
private isTrendingSortEnabled (sort: 'most-viewed' | 'hot' | 'most-liked') {
|
|
||||||
const serverConfig = this.serverService.getHTMLConfig()
|
|
||||||
|
|
||||||
return serverConfig.trending.videos.algorithms.enabled.includes(sort)
|
|
||||||
}
|
|
||||||
|
|
||||||
getFilterValue (filter: VideoFilterActive) {
|
getFilterValue (filter: VideoFilterActive) {
|
||||||
if ((filter.key === 'categoryOneOf' || filter.key === 'languageOneOf') && Array.isArray(filter.rawValue)) {
|
if ((filter.key === 'categoryOneOf' || filter.key === 'languageOneOf') && Array.isArray(filter.rawValue)) {
|
||||||
if (filter.rawValue.length > 2) {
|
if (filter.rawValue.length > 2) {
|
||||||
|
|
|
@ -1088,6 +1088,22 @@ client:
|
||||||
# If null, it will be calculated based on network speed
|
# If null, it will be calculated based on network speed
|
||||||
max_chunk_size: null
|
max_chunk_size: null
|
||||||
|
|
||||||
|
browse_videos:
|
||||||
|
# Default sort option
|
||||||
|
# Available options:
|
||||||
|
# '-publishedAt'
|
||||||
|
# '-originallyPublishedAt'
|
||||||
|
# 'name'
|
||||||
|
# '-trending' (requires the 'most-viewed' trending videos algorithm to be enabled)
|
||||||
|
# '-hot' (requires the 'hot' trending videos algorithm to be enabled)
|
||||||
|
# '-likes' (requires the 'most-liked' trending videos algorithm to be enabled)
|
||||||
|
# '-views'
|
||||||
|
default_sort: '-publishedAt'
|
||||||
|
|
||||||
|
# Default scope option
|
||||||
|
# Available options: 'local' or 'federated'
|
||||||
|
default_scope: 'federated'
|
||||||
|
|
||||||
menu:
|
menu:
|
||||||
login:
|
login:
|
||||||
# If you enable only one external auth plugin
|
# If you enable only one external auth plugin
|
||||||
|
|
|
@ -1095,6 +1095,22 @@ client:
|
||||||
# If null, it will be calculated based on network speed
|
# If null, it will be calculated based on network speed
|
||||||
max_chunk_size: null
|
max_chunk_size: null
|
||||||
|
|
||||||
|
browse_videos:
|
||||||
|
# Default sort option
|
||||||
|
# Available options:
|
||||||
|
# '-publishedAt'
|
||||||
|
# '-originallyPublishedAt'
|
||||||
|
# 'name'
|
||||||
|
# '-trending' (requires the 'most-viewed' trending videos algorithm to be enabled)
|
||||||
|
# '-hot' (requires the 'hot' trending videos algorithm to be enabled)
|
||||||
|
# '-likes' (requires the 'most-liked' trending videos algorithm to be enabled)
|
||||||
|
# '-views'
|
||||||
|
default_sort: '-publishedAt'
|
||||||
|
|
||||||
|
# Default scope option
|
||||||
|
# Available options: 'local' or 'federated'
|
||||||
|
default_scope: 'federated'
|
||||||
|
|
||||||
menu:
|
menu:
|
||||||
login:
|
login:
|
||||||
# If you enable only one external auth plugin
|
# If you enable only one external auth plugin
|
||||||
|
|
|
@ -91,6 +91,11 @@ export interface CustomConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
browseVideos: {
|
||||||
|
defaultSort: string
|
||||||
|
defaultScope: string
|
||||||
|
}
|
||||||
|
|
||||||
menu: {
|
menu: {
|
||||||
login: {
|
login: {
|
||||||
redirectOnSingleExternalAuth: boolean
|
redirectOnSingleExternalAuth: boolean
|
||||||
|
|
|
@ -50,6 +50,11 @@ export interface ServerConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
browseVideos: {
|
||||||
|
defaultSort: string
|
||||||
|
defaultScope: string
|
||||||
|
}
|
||||||
|
|
||||||
menu: {
|
menu: {
|
||||||
login: {
|
login: {
|
||||||
redirectOnSingleExternalAuth: boolean
|
redirectOnSingleExternalAuth: boolean
|
||||||
|
|
|
@ -221,6 +221,69 @@ describe('Test config API validators', function () {
|
||||||
expectedStatus: HttpStatusCode.OK_200
|
expectedStatus: HttpStatusCode.OK_200
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Browse videos section', function () {
|
||||||
|
it('Should fail with an invalid default sort', async function () {
|
||||||
|
const newUpdateParams: CustomConfig = merge({}, {}, updateParams, {
|
||||||
|
client: {
|
||||||
|
browseVideos: {
|
||||||
|
defaultSort: 'hello'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await makePutBodyRequest({
|
||||||
|
url: server.url,
|
||||||
|
path,
|
||||||
|
fields: newUpdateParams,
|
||||||
|
token: server.accessToken,
|
||||||
|
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with a trending default sort & disabled trending algorithm', async function () {
|
||||||
|
const newUpdateParams: CustomConfig = merge({}, {}, updateParams, {
|
||||||
|
trending: {
|
||||||
|
videos: {
|
||||||
|
algorithms: {
|
||||||
|
enabled: [ 'hot', 'most-liked' ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
client: {
|
||||||
|
browseVideos: {
|
||||||
|
defaultSort: '-trending'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await makePutBodyRequest({
|
||||||
|
url: server.url,
|
||||||
|
path,
|
||||||
|
fields: newUpdateParams,
|
||||||
|
token: server.accessToken,
|
||||||
|
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with an invalid default scope', async function () {
|
||||||
|
const newUpdateParams: CustomConfig = merge({}, {}, updateParams, {
|
||||||
|
client: {
|
||||||
|
browseVideos: {
|
||||||
|
defaultScope: 'hello'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await makePutBodyRequest({
|
||||||
|
url: server.url,
|
||||||
|
path,
|
||||||
|
fields: newUpdateParams,
|
||||||
|
token: server.accessToken,
|
||||||
|
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('When deleting the configuration', function () {
|
describe('When deleting the configuration', function () {
|
||||||
|
|
|
@ -50,6 +50,8 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) {
|
||||||
|
|
||||||
expect(data.client.header.hideInstanceName).to.be.false
|
expect(data.client.header.hideInstanceName).to.be.false
|
||||||
expect(data.client.videos.miniature.preferAuthorDisplayName).to.be.false
|
expect(data.client.videos.miniature.preferAuthorDisplayName).to.be.false
|
||||||
|
expect(data.client.browseVideos.defaultSort).to.equal('-publishedAt')
|
||||||
|
expect(data.client.browseVideos.defaultScope).to.equal('federated')
|
||||||
expect(data.client.menu.login.redirectOnSingleExternalAuth).to.be.false
|
expect(data.client.menu.login.redirectOnSingleExternalAuth).to.be.false
|
||||||
|
|
||||||
expect(data.cache.previews.size).to.equal(1)
|
expect(data.cache.previews.size).to.equal(1)
|
||||||
|
@ -233,6 +235,10 @@ function buildNewCustomConfig (server: PeerTubeServer): CustomConfig {
|
||||||
preferAuthorDisplayName: true
|
preferAuthorDisplayName: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
browseVideos: {
|
||||||
|
defaultSort: '-trending',
|
||||||
|
defaultScope: 'local'
|
||||||
|
},
|
||||||
menu: {
|
menu: {
|
||||||
login: {
|
login: {
|
||||||
redirectOnSingleExternalAuth: true
|
redirectOnSingleExternalAuth: true
|
||||||
|
|
|
@ -366,6 +366,10 @@ function customConfig (): CustomConfig {
|
||||||
preferAuthorDisplayName: CONFIG.CLIENT.VIDEOS.MINIATURE.PREFER_AUTHOR_DISPLAY_NAME
|
preferAuthorDisplayName: CONFIG.CLIENT.VIDEOS.MINIATURE.PREFER_AUTHOR_DISPLAY_NAME
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
browseVideos: {
|
||||||
|
defaultSort: CONFIG.CLIENT.BROWSE_VIDEOS.DEFAULT_SORT,
|
||||||
|
defaultScope: CONFIG.CLIENT.BROWSE_VIDEOS.DEFAULT_SCOPE
|
||||||
|
},
|
||||||
menu: {
|
menu: {
|
||||||
login: {
|
login: {
|
||||||
redirectOnSingleExternalAuth: CONFIG.CLIENT.MENU.LOGIN.REDIRECT_ON_SINGLE_EXTERNAL_AUTH
|
redirectOnSingleExternalAuth: CONFIG.CLIENT.MENU.LOGIN.REDIRECT_ON_SINGLE_EXTERNAL_AUTH
|
||||||
|
|
35
server/core/helpers/custom-validators/browse-videos.ts
Normal file
35
server/core/helpers/custom-validators/browse-videos.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { t } from '../i18n.js'
|
||||||
|
|
||||||
|
export function getBrowseVideosDefaultSortError (value: string, enabledTrendingAlgorithms: string[], language: string) {
|
||||||
|
const availableOptions = [ '-publishedAt', '-originallyPublishedAt', 'name', '-trending', '-hot', '-likes', '-views' ]
|
||||||
|
|
||||||
|
if (availableOptions.includes(value) === false) {
|
||||||
|
return t('Browse videos default sort should be \'' + availableOptions.join('\' or \'') + '\', instead of \'' + value + '\'', language)
|
||||||
|
}
|
||||||
|
|
||||||
|
const trendingSortAlgorithmMap = new Map<string, string>([
|
||||||
|
[ '-trending', 'most-viewed' ],
|
||||||
|
[ '-hot', 'hot' ],
|
||||||
|
[ '-likes', 'most-liked' ]
|
||||||
|
])
|
||||||
|
const currentTrendingSortAlgorithm = trendingSortAlgorithmMap.get(value)
|
||||||
|
|
||||||
|
if (currentTrendingSortAlgorithm && enabledTrendingAlgorithms.includes(currentTrendingSortAlgorithm) === false) {
|
||||||
|
return t(
|
||||||
|
`Trending videos algorithm '${currentTrendingSortAlgorithm}' should be enabled if browse videos default sort is '${value}'`,
|
||||||
|
language
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBrowseVideosDefaultScopeError (value: string, language: string) {
|
||||||
|
const availableOptions = [ 'local', 'federated' ]
|
||||||
|
|
||||||
|
if (availableOptions.includes(value) === false) {
|
||||||
|
return t('Browse videos default scope should be \'' + availableOptions.join('\' or \'') + '\', instead of \'' + value + '\'', language)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
|
@ -34,15 +34,16 @@ export async function initI18n () {
|
||||||
.then(() => logger.info('i18n initialized with locales: ' + Object.keys(resources).join(', ')))
|
.then(() => logger.info('i18n initialized with locales: ' + Object.keys(resources).join(', ')))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export type TranslateFn = (key: string, context: Record<string, string | number>) => string
|
||||||
|
|
||||||
export function useI18n (req: express.Request, res: express.Response, next: express.NextFunction) {
|
export function useI18n (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||||
req.t = (key: string, context: Record<string, string | number> = {}) => {
|
req.t = (key: string, context: Record<string, string | number> = {}) => {
|
||||||
// Use req special header language
|
// Use req special header language
|
||||||
// Or user language
|
// Or user language
|
||||||
// Or default language
|
// Or default language
|
||||||
const language = req.headers[LANGUAGE_HEADER] as string ||
|
const language = guessLanguageFromReq(req, res)
|
||||||
res.locals.oauth?.token?.User?.language ||
|
|
||||||
req.acceptsLanguages(AVAILABLE_LOCALES) ||
|
|
||||||
CONFIG.INSTANCE.DEFAULT_LANGUAGE
|
|
||||||
|
|
||||||
return t(key, language, context)
|
return t(key, language, context)
|
||||||
}
|
}
|
||||||
|
@ -50,6 +51,13 @@ export function useI18n (req: express.Request, res: express.Response, next: expr
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function guessLanguageFromReq (req: express.Request, res: express.Response) {
|
||||||
|
return req.headers[LANGUAGE_HEADER] as string ||
|
||||||
|
res.locals.oauth?.token?.User?.language ||
|
||||||
|
req.acceptsLanguages(AVAILABLE_LOCALES) ||
|
||||||
|
CONFIG.INSTANCE.DEFAULT_LANGUAGE
|
||||||
|
}
|
||||||
|
|
||||||
export function t (key: string, language: string, context: Record<string, string | number> = {}) {
|
export function t (key: string, language: string, context: Record<string, string | number> = {}) {
|
||||||
if (!language) throw new Error('Language is required for translation')
|
if (!language) throw new Error('Language is required for translation')
|
||||||
|
|
||||||
|
@ -74,3 +82,7 @@ export function setClientLanguageCookie (res: express.Response, language: string
|
||||||
maxAge: 1000 * 3600 * 24 * 90 // 3 months
|
maxAge: 1000 * 3600 * 24 * 90 // 3 months
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const englishLanguage = 'en-US'
|
||||||
|
|
|
@ -8,12 +8,14 @@ import { basename } from 'path'
|
||||||
import { URL } from 'url'
|
import { URL } from 'url'
|
||||||
import { parseBytes, parseSemVersion } from '../helpers/core-utils.js'
|
import { parseBytes, parseSemVersion } from '../helpers/core-utils.js'
|
||||||
import { isArray } from '../helpers/custom-validators/misc.js'
|
import { isArray } from '../helpers/custom-validators/misc.js'
|
||||||
|
import { getBrowseVideosDefaultSortError, getBrowseVideosDefaultScopeError } from '../helpers/custom-validators/browse-videos.js'
|
||||||
import { logger } from '../helpers/logger.js'
|
import { logger } from '../helpers/logger.js'
|
||||||
import { ApplicationModel, getServerActor } from '../models/application/application.js'
|
import { ApplicationModel, getServerActor } from '../models/application/application.js'
|
||||||
import { OAuthClientModel } from '../models/oauth/oauth-client.js'
|
import { OAuthClientModel } from '../models/oauth/oauth-client.js'
|
||||||
import { UserModel } from '../models/user/user.js'
|
import { UserModel } from '../models/user/user.js'
|
||||||
import { CONFIG, getLocalConfigFilePath, isEmailEnabled, reloadConfig } from './config.js'
|
import { CONFIG, getLocalConfigFilePath, isEmailEnabled, reloadConfig } from './config.js'
|
||||||
import { WEBSERVER } from './constants.js'
|
import { WEBSERVER } from './constants.js'
|
||||||
|
import { englishLanguage } from '@server/helpers/i18n.js'
|
||||||
|
|
||||||
async function checkActivityPubUrls () {
|
async function checkActivityPubUrls () {
|
||||||
const actor = await getServerActor()
|
const actor = await getServerActor()
|
||||||
|
@ -54,6 +56,7 @@ function checkConfig () {
|
||||||
checkObjectStorageConfig()
|
checkObjectStorageConfig()
|
||||||
checkVideoStudioConfig()
|
checkVideoStudioConfig()
|
||||||
checkThumbnailsConfig()
|
checkThumbnailsConfig()
|
||||||
|
checkBrowseVideosConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
// We get db by param to not import it in this file (import orders)
|
// We get db by param to not import it in this file (import orders)
|
||||||
|
@ -385,3 +388,16 @@ function checkThumbnailsConfig () {
|
||||||
throw new Error('thumbnails.sizes must be an array of 2 sizes')
|
throw new Error('thumbnails.sizes must be an array of 2 sizes')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkBrowseVideosConfig () {
|
||||||
|
const sortError = getBrowseVideosDefaultSortError(
|
||||||
|
CONFIG.CLIENT.BROWSE_VIDEOS.DEFAULT_SORT,
|
||||||
|
CONFIG.TRENDING.VIDEOS.ALGORITHMS.ENABLED,
|
||||||
|
englishLanguage
|
||||||
|
)
|
||||||
|
|
||||||
|
if (sortError) throw new Error(sortError)
|
||||||
|
|
||||||
|
const scopeError = getBrowseVideosDefaultScopeError(CONFIG.CLIENT.BROWSE_VIDEOS.DEFAULT_SCOPE, englishLanguage)
|
||||||
|
if (scopeError) throw new Error(scopeError)
|
||||||
|
}
|
||||||
|
|
|
@ -125,6 +125,8 @@ export function checkMissedConfig () {
|
||||||
'auto_blacklist.videos.of_users.enabled',
|
'auto_blacklist.videos.of_users.enabled',
|
||||||
'trending.videos.interval_days',
|
'trending.videos.interval_days',
|
||||||
'client.videos.miniature.prefer_author_display_name',
|
'client.videos.miniature.prefer_author_display_name',
|
||||||
|
'client.browse_videos.default_sort',
|
||||||
|
'client.browse_videos.default_scope',
|
||||||
'client.menu.login.redirect_on_single_external_auth',
|
'client.menu.login.redirect_on_single_external_auth',
|
||||||
'client.header.hide_instance_name',
|
'client.header.hide_instance_name',
|
||||||
'defaults.publish.download_enabled',
|
'defaults.publish.download_enabled',
|
||||||
|
|
|
@ -95,6 +95,14 @@ const CONFIG = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
BROWSE_VIDEOS: {
|
||||||
|
get DEFAULT_SORT () {
|
||||||
|
return config.get<string>('client.browse_videos.default_sort')
|
||||||
|
},
|
||||||
|
get DEFAULT_SCOPE () {
|
||||||
|
return config.get<string>('client.browse_videos.default_scope')
|
||||||
|
}
|
||||||
|
},
|
||||||
MENU: {
|
MENU: {
|
||||||
LOGIN: {
|
LOGIN: {
|
||||||
get REDIRECT_ON_SINGLE_EXTERNAL_AUTH () {
|
get REDIRECT_ON_SINGLE_EXTERNAL_AUTH () {
|
||||||
|
|
|
@ -69,6 +69,10 @@ class ServerConfigManager {
|
||||||
maxChunkSize: CONFIG.CLIENT.VIDEOS.RESUMABLE_UPLOAD.MAX_CHUNK_SIZE
|
maxChunkSize: CONFIG.CLIENT.VIDEOS.RESUMABLE_UPLOAD.MAX_CHUNK_SIZE
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
browseVideos: {
|
||||||
|
defaultSort: CONFIG.CLIENT.BROWSE_VIDEOS.DEFAULT_SORT,
|
||||||
|
defaultScope: CONFIG.CLIENT.BROWSE_VIDEOS.DEFAULT_SCOPE
|
||||||
|
},
|
||||||
menu: {
|
menu: {
|
||||||
login: {
|
login: {
|
||||||
redirectOnSingleExternalAuth: CONFIG.CLIENT.MENU.LOGIN.REDIRECT_ON_SINGLE_EXTERNAL_AUTH
|
redirectOnSingleExternalAuth: CONFIG.CLIENT.MENU.LOGIN.REDIRECT_ON_SINGLE_EXTERNAL_AUTH
|
||||||
|
|
|
@ -3,9 +3,11 @@ import { isConfigLogoTypeValid } from '@server/helpers/custom-validators/config.
|
||||||
import { isIntOrNull } from '@server/helpers/custom-validators/misc.js'
|
import { isIntOrNull } from '@server/helpers/custom-validators/misc.js'
|
||||||
import { isNumberArray, isStringArray } from '@server/helpers/custom-validators/search.js'
|
import { isNumberArray, isStringArray } from '@server/helpers/custom-validators/search.js'
|
||||||
import { isVideoCommentsPolicyValid, isVideoLicenceValid, isVideoPrivacyValid } from '@server/helpers/custom-validators/videos.js'
|
import { isVideoCommentsPolicyValid, isVideoLicenceValid, isVideoPrivacyValid } from '@server/helpers/custom-validators/videos.js'
|
||||||
|
import { guessLanguageFromReq } from '@server/helpers/i18n.js'
|
||||||
import { CONFIG, isEmailEnabled } from '@server/initializers/config.js'
|
import { CONFIG, isEmailEnabled } from '@server/initializers/config.js'
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { body, param } from 'express-validator'
|
import { body, param } from 'express-validator'
|
||||||
|
import { getBrowseVideosDefaultScopeError, getBrowseVideosDefaultSortError } from '../../helpers/custom-validators/browse-videos.js'
|
||||||
import { isThemeNameValid } from '../../helpers/custom-validators/plugins.js'
|
import { isThemeNameValid } from '../../helpers/custom-validators/plugins.js'
|
||||||
import { isUserNSFWPolicyValid, isUserVideoQuotaDailyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users.js'
|
import { isUserNSFWPolicyValid, isUserVideoQuotaDailyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users.js'
|
||||||
import { isThemeRegistered } from '../../lib/plugins/theme-utils.js'
|
import { isThemeRegistered } from '../../lib/plugins/theme-utils.js'
|
||||||
|
@ -159,6 +161,7 @@ export const customConfigUpdateValidator = [
|
||||||
if (!checkInvalidLiveConfig(req.body, req, res)) return
|
if (!checkInvalidLiveConfig(req.body, req, res)) return
|
||||||
if (!checkInvalidVideoStudioConfig(req.body, req, res)) return
|
if (!checkInvalidVideoStudioConfig(req.body, req, res)) return
|
||||||
if (!checkInvalidSearchConfig(req.body, req, res)) return
|
if (!checkInvalidSearchConfig(req.body, req, res)) return
|
||||||
|
if (!checkInvalidBrowseVideosConfig(req.body, req, res)) return
|
||||||
|
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
|
@ -254,3 +257,23 @@ function checkInvalidSearchConfig (customConfig: CustomConfig, req: express.Requ
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkInvalidBrowseVideosConfig (customConfig: CustomConfig, req: express.Request, res: express.Response) {
|
||||||
|
const sortError = getBrowseVideosDefaultSortError(
|
||||||
|
customConfig.client.browseVideos.defaultSort,
|
||||||
|
customConfig.trending.videos.algorithms.enabled,
|
||||||
|
guessLanguageFromReq(req, res)
|
||||||
|
)
|
||||||
|
if (sortError) {
|
||||||
|
res.fail({ message: sortError })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const scopeError = getBrowseVideosDefaultScopeError(customConfig.client.browseVideos.defaultScope, guessLanguageFromReq(req, res))
|
||||||
|
if (scopeError) {
|
||||||
|
res.fail({ message: scopeError })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue